From df122310a059c1a33d322fc952297c0ce5d72e40 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 16 Jan 2024 17:15:40 +0100 Subject: [PATCH 001/483] feat(catalog): allow setting EntityDataParser using CatalogProcessingExtensionPoint Signed-off-by: secustor --- .changeset/polite-zoos-pay.md | 6 ++++++ plugins/catalog-backend/src/service/CatalogPlugin.ts | 9 +++++++++ plugins/catalog-node/api-report-alpha.md | 3 +++ plugins/catalog-node/src/extensions.ts | 2 ++ 4 files changed, 20 insertions(+) create mode 100644 .changeset/polite-zoos-pay.md diff --git a/.changeset/polite-zoos-pay.md b/.changeset/polite-zoos-pay.md new file mode 100644 index 0000000000..1a6a0c69a7 --- /dev/null +++ b/.changeset/polite-zoos-pay.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-node': minor +--- + +Allow setting EntityDataParser using CatalogProcessingExtensionPoint diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 0800d41340..10ca57d7e1 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -28,11 +28,13 @@ import { } from '@backstage/plugin-catalog-node/alpha'; import { CatalogProcessor, + CatalogProcessorParser, EntityProvider, ScmLocationAnalyzer, } from '@backstage/plugin-catalog-node'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { PlaceholderResolver } from '../modules'; +import { defaultEntityDataParser } from '../modules/util/parse'; class CatalogProcessingExtensionPointImpl implements CatalogProcessingExtensionPoint @@ -40,6 +42,7 @@ class CatalogProcessingExtensionPointImpl #processors = new Array(); #entityProviders = new Array(); #placeholderResolvers: Record = {}; + entityDataParser: CatalogProcessorParser = defaultEntityDataParser; addProcessor( ...processors: Array> @@ -61,6 +64,10 @@ class CatalogProcessingExtensionPointImpl this.#placeholderResolvers[key] = resolver; } + setEntityDataParser(parser: CatalogProcessorParser): void { + this.entityDataParser = parser; + } + get processors() { return this.#processors; } @@ -164,6 +171,8 @@ export const catalogPlugin = createBackendPlugin({ }); builder.addProcessor(...processingExtensions.processors); builder.addEntityProvider(...processingExtensions.entityProviders); + builder.setEntityDataParser(processingExtensions.entityDataParser); + Object.entries(processingExtensions.placeholderResolvers).forEach( ([key, resolver]) => builder.setPlaceholderResolver(key, resolver), ); diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index 74339b1bc9..a231e43885 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -5,6 +5,7 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; +import { CatalogProcessorParser } from '@backstage/plugin-catalog-node'; import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-node'; @@ -54,6 +55,8 @@ export interface CatalogProcessingExtensionPoint { addProcessor( ...processors: Array> ): void; + // (undocumented) + setEntityDataParser(parser: CatalogProcessorParser): void; } // @alpha (undocumented) diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 7aa9264030..e52468bcab 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -18,6 +18,7 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { CatalogProcessor, + CatalogProcessorParser, EntitiesSearchFilter, EntityProvider, PlaceholderResolver, @@ -37,6 +38,7 @@ export interface CatalogProcessingExtensionPoint { ...providers: Array> ): void; addPlaceholderResolver(key: string, resolver: PlaceholderResolver): void; + setEntityDataParser(parser: CatalogProcessorParser): void; } /** From 7cfcddd9aaf3828c854799e5cc3635abba2708cc Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 18 Jan 2024 00:00:44 +0100 Subject: [PATCH 002/483] feat: throw error if extensions point tries to set data parser multiple times Signed-off-by: secustor --- plugins/catalog-backend/src/service/CatalogPlugin.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 10ca57d7e1..91d3fa0ecc 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -42,7 +42,7 @@ class CatalogProcessingExtensionPointImpl #processors = new Array(); #entityProviders = new Array(); #placeholderResolvers: Record = {}; - entityDataParser: CatalogProcessorParser = defaultEntityDataParser; + entityDataParser?: CatalogProcessorParser; addProcessor( ...processors: Array> @@ -65,6 +65,11 @@ class CatalogProcessingExtensionPointImpl } setEntityDataParser(parser: CatalogProcessorParser): void { + if (this.entityDataParser) { + throw new Error( + 'Attempted to install second EntityDataParser. Only one can be set.', + ); + } this.entityDataParser = parser; } @@ -171,7 +176,9 @@ export const catalogPlugin = createBackendPlugin({ }); builder.addProcessor(...processingExtensions.processors); builder.addEntityProvider(...processingExtensions.entityProviders); - builder.setEntityDataParser(processingExtensions.entityDataParser); + builder.setEntityDataParser( + processingExtensions.entityDataParser ?? defaultEntityDataParser, + ); Object.entries(processingExtensions.placeholderResolvers).forEach( ([key, resolver]) => builder.setPlaceholderResolver(key, resolver), From c66fce65c43428c1944b2a1ac35db5840f6722f2 Mon Sep 17 00:00:00 2001 From: secustor Date: Tue, 23 Jan 2024 16:41:16 +0100 Subject: [PATCH 003/483] refactor(catalog): use private field and rely on default parser provided by build() Signed-off-by: secustor --- .../src/service/CatalogPlugin.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 91d3fa0ecc..c65dadafae 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -34,7 +34,6 @@ import { } from '@backstage/plugin-catalog-node'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { PlaceholderResolver } from '../modules'; -import { defaultEntityDataParser } from '../modules/util/parse'; class CatalogProcessingExtensionPointImpl implements CatalogProcessingExtensionPoint @@ -42,7 +41,7 @@ class CatalogProcessingExtensionPointImpl #processors = new Array(); #entityProviders = new Array(); #placeholderResolvers: Record = {}; - entityDataParser?: CatalogProcessorParser; + #entityDataParser?: CatalogProcessorParser; addProcessor( ...processors: Array> @@ -65,12 +64,12 @@ class CatalogProcessingExtensionPointImpl } setEntityDataParser(parser: CatalogProcessorParser): void { - if (this.entityDataParser) { + if (this.#entityDataParser) { throw new Error( 'Attempted to install second EntityDataParser. Only one can be set.', ); } - this.entityDataParser = parser; + this.#entityDataParser = parser; } get processors() { @@ -84,6 +83,10 @@ class CatalogProcessingExtensionPointImpl get placeholderResolvers() { return this.#placeholderResolvers; } + + get entityDataParser() { + return this.#entityDataParser; + } } class CatalogAnalysisExtensionPointImpl @@ -176,9 +179,10 @@ export const catalogPlugin = createBackendPlugin({ }); builder.addProcessor(...processingExtensions.processors); builder.addEntityProvider(...processingExtensions.entityProviders); - builder.setEntityDataParser( - processingExtensions.entityDataParser ?? defaultEntityDataParser, - ); + + if (processingExtensions.entityDataParser) { + builder.setEntityDataParser(processingExtensions.entityDataParser); + } Object.entries(processingExtensions.placeholderResolvers).forEach( ([key, resolver]) => builder.setPlaceholderResolver(key, resolver), From 5211dd8d918cfa63cdd47f559cbba549460a16bb Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Fri, 26 Jan 2024 15:44:30 +0100 Subject: [PATCH 004/483] fix: fix decoding issues in StackOverflowSearchResultListItem Signed-off-by: Tommy Le --- plugins/stack-overflow/package.json | 2 ++ .../StackOverflowSearchResultListItem.tsx | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index de0aec1e61..5364ee59bb 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -54,6 +54,7 @@ "@testing-library/jest-dom": "^6.0.0", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^4.0.0", + "he": "^1.2.0", "lodash": "^4.17.21", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -69,6 +70,7 @@ "@testing-library/dom": "^9.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", + "@types/he": "^1.2.3", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx index 245666deeb..9e98d6612f 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx @@ -15,7 +15,6 @@ */ import React from 'react'; -import _unescape from 'lodash/unescape'; import { Link } from '@backstage/core-components'; import { Divider, @@ -26,8 +25,9 @@ import { Chip, } from '@material-ui/core'; import { useAnalytics } from '@backstage/core-plugin-api'; -import { ResultHighlight } from '@backstage/plugin-search-common'; +import type { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { decode } from 'he'; /** * Props for {@link StackOverflowSearchResultListItem} @@ -48,6 +48,10 @@ export const StackOverflowSearchResultListItem = ( const analytics = useAnalytics(); const handleClick = () => { + if (!result) { + return; + } + analytics.captureEvent('discover', result.title, { attributes: { to: result.location }, value: props.rank, @@ -69,12 +73,12 @@ export const StackOverflowSearchResultListItem = ( {highlight?.fields?.title ? ( ) : ( - _unescape(result.title) + decode(result.title) )} } @@ -83,13 +87,13 @@ export const StackOverflowSearchResultListItem = ( <> Author:{' '} ) : ( - `Author: ${result.text}` + `Author: ${decode(result.text)}` ) } /> From c6779aca2da8103128a257afc18e1853f3797e90 Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Fri, 26 Jan 2024 15:49:18 +0100 Subject: [PATCH 005/483] chore: add changeset Signed-off-by: Tommy Le --- .changeset/dry-impalas-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-impalas-serve.md diff --git a/.changeset/dry-impalas-serve.md b/.changeset/dry-impalas-serve.md new file mode 100644 index 0000000000..709c0fb498 --- /dev/null +++ b/.changeset/dry-impalas-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +fix: fix decode issues in title and author fields in StackOverflowSearchResultListItem From e8ec64ba964da8ad49797e5b88ef52255f51913d Mon Sep 17 00:00:00 2001 From: Antonio Ereiz Date: Thu, 1 Feb 2024 21:37:24 +0100 Subject: [PATCH 006/483] poc Signed-off-by: Antonio Ereiz --- docs/tutorials/setup-opentelemetry.md | 63 +++++++++++++++++++++------ 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index aa96e2ad40..fa1d197e60 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -24,16 +24,18 @@ yarn --cwd packages/backend add @opentelemetry/sdk-node \ ## Configure -In your `packages/backend/src` folder, create an `instrumentation.ts` file. +In your `packages/backend` folder, create an `instrumentation.ts` file. ```typescript -import { NodeSDK } from '@opentelemetry/sdk-node'; -import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; -import { +const { NodeSDK } = require('@opentelemetry/sdk-node'); +const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node'); +const { + getNodeAutoInstrumentations, +} = require('@opentelemetry/auto-instrumentations-node'); +const { PeriodicExportingMetricReader, ConsoleMetricExporter, -} from '@opentelemetry/sdk-metrics'; +} = require('@opentelemetry/sdk-metrics'); const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), @@ -46,23 +48,56 @@ const sdk = new NodeSDK({ sdk.start(); ``` -In the `index.ts`, import this file **at the beginning**: - -```typescript -import './instrumentation'; // Setup the OpenTelemetry instrumentation - -// other imports and backend init... -``` +Your probably won't need all the instrumentations inside `getNodeAutoInstrumentations()` so make sure to +check the [documentation](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) and tweak it properly. It's important to setup the NodeSDK and the automatic instrumentation **before** importing any library. +This is why we will use the nodejs [`--require`](https://nodejs.org/api/cli.html#-r---require-module) +flag when we start up the application. + +In your `Dockerfile` add the `--require` flag which points to the `instrumentation.ts` file + +```Dockerfile +FROM node:18-bookworm-slim +... +WORKDIR /app +RUN chown node:node /app +USER node + +ENV NODE_ENV production + +COPY --chown=node:node .yarn ./.yarn +COPY --chown=node:node .yarnrc.yml ./ + +# We need the instrumentation file inside the Docker image so we can use it with --require +// highlight-add-next-line +COPY --chown=node:node packages/backend/instrumentation.ts ./ + +COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,sharing=locked,uid=1000,gid=1000 \ + yarn workspaces focus --all --production + +COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +// highlight-remove-next-line +CMD ["node", "packages/backend", "--config", "app-config.yaml"] +// highlight-add-next-line +CMD ["node", "packages/backend", "--require", "./instrumentation.ts" "--config", "app-config.yaml"] +``` + ## Run Backstage You can now start your Backstage instance as usual, using `yarn dev`. When the backend is started, you should see in your console traces and metrics emitted by OpenTelemetry. -Of course in production you probably won't use the console exporters but instead send traces and metrics to an OpenTelemetry Collector using [OTLP exporters](https://opentelemetry.io/docs/instrumentation/js/exporters/). +Of course in production you probably won't use the console exporters but instead send traces and metrics to an OpenTelemetry Collector or other exporter using [OTLP exporters](https://opentelemetry.io/docs/instrumentation/js/exporters/). + +If you need to disable/configure some Opentelemetry feature there are lots of [environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) which you can tweak. ## References From 5b8efa4e062e0e507511d9517a3c633b998a1286 Mon Sep 17 00:00:00 2001 From: Antonio Ereiz Date: Fri, 2 Feb 2024 20:37:44 +0100 Subject: [PATCH 007/483] refactor Signed-off-by: Antonio Ereiz --- docs/tutorials/setup-opentelemetry.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index fa1d197e60..ac61f12d33 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -48,7 +48,7 @@ const sdk = new NodeSDK({ sdk.start(); ``` -Your probably won't need all the instrumentations inside `getNodeAutoInstrumentations()` so make sure to +Your probably won't need all the instrumentation inside `getNodeAutoInstrumentations()` so make sure to check the [documentation](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) and tweak it properly. It's important to setup the NodeSDK and the automatic instrumentation **before** importing any library. @@ -61,6 +61,8 @@ In your `Dockerfile` add the `--require` flag which points to the `instrumentati ```Dockerfile FROM node:18-bookworm-slim ... +# More functionality goes here +... WORKDIR /app RUN chown node:node /app USER node @@ -86,18 +88,28 @@ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz // highlight-remove-next-line CMD ["node", "packages/backend", "--config", "app-config.yaml"] // highlight-add-next-line -CMD ["node", "packages/backend", "--require", "./instrumentation.ts" "--config", "app-config.yaml"] +CMD ["node", "--require", "./instrumentation.ts", "packages/backend", "--config", "app-config.yaml"] ``` ## Run Backstage +The above configuration will only work in production once your start a Docker container from the image. + +To be able to test locally you can import the `./instrumentation.ts` file at the top (before all imports) of your backend `index.ts` file + +```ts +import '../instrumentation.ts' +// Other imports +... +``` + You can now start your Backstage instance as usual, using `yarn dev`. When the backend is started, you should see in your console traces and metrics emitted by OpenTelemetry. Of course in production you probably won't use the console exporters but instead send traces and metrics to an OpenTelemetry Collector or other exporter using [OTLP exporters](https://opentelemetry.io/docs/instrumentation/js/exporters/). -If you need to disable/configure some Opentelemetry feature there are lots of [environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) which you can tweak. +If you need to disable/configure some OpenTelemetry feature there are lots of [environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) which you can tweak. ## References From acb684e1c63e96a17b7de89b01f5f196807c8bc4 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 4 Feb 2024 19:55:02 +0100 Subject: [PATCH 008/483] Checkpoint Signed-off-by: bnechyporenko --- .../package.json | 2 + .../src/actions/github.ts | 1 + .../src/actions/helpers.ts | 169 +++++++++++------- .../migrations/20240203232000_state.js | 35 ++++ plugins/scaffolder-backend/src/index.ts | 1 + .../src/scaffolder/tasks/DatabaseTaskStore.ts | 13 ++ .../tasks/NunjucksWorkflowRunner.ts | 8 + .../src/scaffolder/tasks/StorageTaskBroker.ts | 14 +- .../src/scaffolder/tasks/types.ts | 9 +- .../src/util/defineCheckpoint.ts | 34 ++++ plugins/scaffolder-node/src/actions/types.ts | 4 + plugins/scaffolder-node/src/tasks/index.ts | 1 + plugins/scaffolder-node/src/tasks/types.ts | 10 ++ yarn.lock | 2 + 14 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 plugins/scaffolder-backend/migrations/20240203232000_state.js create mode 100644 plugins/scaffolder-backend/src/util/defineCheckpoint.ts diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index a0cccb536c..f48feaaae5 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -37,7 +37,9 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-scaffolder-backend": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/types": "workspace:^", "@octokit/webhooks": "^10.0.0", "libsodium-wrappers": "^0.7.11", "octokit": "^3.0.0", diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 91d7a34aea..ed55714852 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -244,6 +244,7 @@ export function createPublishGithubAction(options: { repoVariables, secrets, ctx.logger, + ctx.checkpoint, ); const remoteUrl = 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 4d95a6494f..87c125eda2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -36,6 +36,8 @@ import { enableBranchProtectionOnDefaultRepoBranch, entityRefToName, } from './gitHelpers'; +import { JsonObject } from '@backstage/types'; +import { defineCheckpoint } from '@backstage/plugin-scaffolder-backend'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -138,6 +140,10 @@ export async function createGithubRepoWithCollaboratorsAndTopics( repoVariables: { [key: string]: string } | undefined, secrets: { [key: string]: string } | undefined, logger: Logger, + checkpoint?: ( + key: string, + fn: () => Promise, + ) => Promise, ) { // eslint-disable-next-line testing-library/no-await-sync-queries const user = await client.rest.users.getByUsername({ @@ -148,59 +154,70 @@ export async function createGithubRepoWithCollaboratorsAndTopics( await validateAccessTeam(client, access); } - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - private: repoVisibility === 'private', - // @ts-ignore https://github.com/octokit/types.ts/issues/522 - visibility: repoVisibility, - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - squash_merge_commit_title: squashMergeCommitTitle, - squash_merge_commit_message: squashMergeCommitMessage, - allow_rebase_merge: allowRebaseMerge, - allow_auto_merge: allowAutoMerge, - homepage: homepage, - has_projects: hasProjects, - has_wiki: hasWiki, - has_issues: hasIssues, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - private: repoVisibility === 'private', - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - squash_merge_commit_title: squashMergeCommitTitle, - squash_merge_commit_message: squashMergeCommitMessage, - allow_rebase_merge: allowRebaseMerge, - allow_auto_merge: allowAutoMerge, - homepage: homepage, - has_projects: hasProjects, - has_wiki: hasWiki, - has_issues: hasIssues, - }); + const repoCreation = async () => { + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + private: repoVisibility === 'private', + // @ts-ignore https://github.com/octokit/types.ts/issues/522 + visibility: repoVisibility, + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + squash_merge_commit_title: squashMergeCommitTitle, + squash_merge_commit_message: squashMergeCommitMessage, + allow_rebase_merge: allowRebaseMerge, + allow_auto_merge: allowAutoMerge, + homepage: homepage, + has_projects: hasProjects, + has_wiki: hasWiki, + has_issues: hasIssues, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + private: repoVisibility === 'private', + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + squash_merge_commit_title: squashMergeCommitTitle, + squash_merge_commit_message: squashMergeCommitMessage, + allow_rebase_merge: allowRebaseMerge, + allow_auto_merge: allowAutoMerge, + homepage: homepage, + has_projects: hasProjects, + has_wiki: hasWiki, + has_issues: hasIssues, + }); - let newRepo; + let newRepo; - try { - newRepo = (await repoCreationPromise).data; - } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { - logger.warn( - `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, + try { + newRepo = (await repoCreationPromise).data; + } catch (e) { + assertError(e); + if (e.message === 'Resource not accessible by integration') { + logger.warn( + `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, + ); + } + throw new Error( + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, ); } - throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, - ); - } + return { newRepo }; + }; + + const { newRepo } = await defineCheckpoint<{ + newRepo: { clone_url: string; html_url: string }; + }>({ + key: 'v1.task.checkpoint.repo.creation', + checkpoint, + fn: repoCreation, + }); if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); @@ -213,11 +230,19 @@ export async function createGithubRepoWithCollaboratorsAndTopics( }); // No need to add access if it's the person who owns the personal account } else if (access && access !== owner) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: access, - permission: 'admin', + const addCollaborator = async () => { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: access, + permission: 'admin', + }); + return {}; + }; + await defineCheckpoint({ + key: 'v1.task.checkpoint.add.collaborator', + checkpoint, + fn: addCollaborator, }); } @@ -225,11 +250,19 @@ export async function createGithubRepoWithCollaboratorsAndTopics( for (const collaborator of collaborators) { try { if ('user' in collaborator) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: entityRefToName(collaborator.user), - permission: collaborator.access, + const addCollaborator = async () => { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: entityRefToName(collaborator.user), + permission: collaborator.access, + }); + return {}; + }; + await defineCheckpoint({ + key: `v1.task.checkpoint.add.collaborator.${collaborator.user}`, + checkpoint, + fn: addCollaborator, }); } else if ('team' in collaborator) { await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ @@ -264,11 +297,19 @@ export async function createGithubRepoWithCollaboratorsAndTopics( } for (const [key, value] of Object.entries(repoVariables ?? {})) { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, + const createRepoVariable = async () => { + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, + }); + return {}; + }; + await defineCheckpoint({ + key: `v1.task.checkpoint.create.repo.variable.${key}`, + checkpoint, + fn: createRepoVariable, }); } diff --git a/plugins/scaffolder-backend/migrations/20240203232000_state.js b/plugins/scaffolder-backend/migrations/20240203232000_state.js new file mode 100644 index 0000000000..9ffea1716e --- /dev/null +++ b/plugins/scaffolder-backend/migrations/20240203232000_state.js @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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.alterTable('tasks', table => { + table.text('state').nullable().comment('A state of the checkpoints'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('tasks', table => { + table.dropColumn('state'); + }); +}; diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index 649a5df233..d59499739e 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -23,5 +23,6 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib'; +export { defineCheckpoint } from './util/defineCheckpoint'; export * from './deprecated'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 766db4646e..e6083a469f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -30,6 +30,7 @@ import { TaskStoreCreateTaskResult, TaskStoreShutDownTaskOptions, TaskStoreRecoverTaskOptions, + TaskStoreStateOptions, } from './types'; import { SerializedTaskEvent, @@ -52,6 +53,7 @@ export type RawDbTaskRow = { id: string; spec: string; status: TaskStatus; + state?: string; last_heartbeat_at?: string; created_at: string; created_by: string | null; @@ -394,6 +396,17 @@ export class DatabaseTaskStore implements TaskStore { }); } + async saveCheckpoint?(options: TaskStoreStateOptions): Promise { + if (options.state) { + const serializedState = JSON.stringify(options.state); + await this.db('tasks') + .where({ id: options.taskId }) + .update({ + state: serializedState, + }); + } + } + async listEvents( options: TaskStoreListEventsOptions, ): Promise<{ events: SerializedTaskEvent[] }> { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index ce1ad71c33..190c880280 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -349,6 +349,14 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { logger: taskLogger, logStream: streamLogger, workspacePath, + async checkpoint( + key: string, + fn: () => Promise, + ) { + const value = await fn(); + task.updateCheckpoint?.(key, value); + return value; + }, createTemporaryDirectory: async () => { const tmpDir = await fs.mkdtemp( `${workspacePath}_step-${step.id}-`, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 8b49f492e8..8762253436 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { TaskSecrets, TaskState } from '@backstage/plugin-scaffolder-node'; import { JsonObject, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; @@ -91,6 +91,14 @@ export class TaskManager implements TaskContext { }); } + async updateCheckpoint?(key: string, value: JsonObject): Promise { + this.task.state = { [key]: value }; + await this.storage.saveCheckpoint?.({ + taskId: this.task.taskId, + state: this.task.state, + }); + } + async complete( result: TaskCompletionState, metadata?: JsonObject, @@ -144,6 +152,10 @@ export interface CurrentClaimedTask { * The secrets that are stored with the task. */ secrets?: TaskSecrets; + /** + * The state of checkpoints of the task. + */ + state?: TaskState; /** * The creator of the task. */ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index c5783ccb49..b6225d46a9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,7 +16,7 @@ import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { TaskSecrets, TaskState } from '@backstage/plugin-scaffolder-node'; import { TemplateAction, TaskStatus as _TaskStatus, @@ -113,6 +113,11 @@ export type TaskStoreEmitOptions = { body: TBody; }; +export type TaskStoreStateOptions = { + taskId: string; + state?: TaskState; +}; + /** * TaskStoreListEventsOptions * @@ -194,6 +199,8 @@ export interface TaskStore { emitLogEvent(options: TaskStoreEmitOptions): Promise; + saveCheckpoint?(options: TaskStoreStateOptions): Promise; + listEvents( options: TaskStoreListEventsOptions, ): Promise<{ events: SerializedTaskEvent[] }>; diff --git a/plugins/scaffolder-backend/src/util/defineCheckpoint.ts b/plugins/scaffolder-backend/src/util/defineCheckpoint.ts new file mode 100644 index 0000000000..6656005684 --- /dev/null +++ b/plugins/scaffolder-backend/src/util/defineCheckpoint.ts @@ -0,0 +1,34 @@ +/* + * 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 { JsonObject } from '@backstage/types'; + +export type DefineCheckpointProps = { + checkpoint?: (key: string, fn: () => Promise) => Promise; + key: string; + fn: () => Promise; +}; + +export const defineCheckpoint = async ( + props: DefineCheckpointProps, +): Promise => { + const { checkpoint, fn, key } = props; + return checkpoint + ? checkpoint?.(key, async () => { + return await fn(); + }) + : fn(); +}; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 7e8c2b4f5f..a7f44c4730 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -35,6 +35,10 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; + checkpoint?( + key: string, + fn: () => Promise, + ): Promise; output( name: keyof TActionOutput, value: TActionOutput[keyof TActionOutput], diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 930de95237..99638e48af 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -24,5 +24,6 @@ export type { TaskCompletionState, TaskContext, TaskEventType, + TaskState, TaskStatus, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 7cdc044bdf..7136211671 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -26,6 +26,13 @@ export type TaskSecrets = Record & { backstageToken?: string; }; +/** + * TaskState + * + * @public + */ +export type TaskState = Record; + /** * The status of each step of the Task * @@ -110,6 +117,7 @@ export interface TaskContext { cancelSignal: AbortSignal; spec: TaskSpec; secrets?: TaskSecrets; + state?: TaskState; createdBy?: string; done: boolean; isDryRun?: boolean; @@ -118,6 +126,8 @@ export interface TaskContext { emitLog(message: string, logMetadata?: JsonObject): Promise; + updateCheckpoint?(key: string, value: JsonObject): Promise; + getWorkspaceName(): Promise; } diff --git a/yarn.lock b/yarn.lock index 8c0ddcabc2..4a18d8c628 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8354,7 +8354,9 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-scaffolder-backend": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/types": "workspace:^" "@octokit/webhooks": ^10.0.0 "@types/libsodium-wrappers": ^0.7.10 fs-extra: 10.1.0 From 245617991c9d43cf3bdeb76c5ca3da45007beaec Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 4 Feb 2024 20:43:59 +0100 Subject: [PATCH 009/483] Checkpoint Signed-off-by: bnechyporenko --- .../tasks/NunjucksWorkflowRunner.ts | 21 +++++++++++--- .../src/scaffolder/tasks/StorageTaskBroker.ts | 14 +++++++-- .../src/util/defineCheckpoint.ts | 8 ++--- plugins/scaffolder-node/src/tasks/index.ts | 1 + plugins/scaffolder-node/src/tasks/types.ts | 29 +++++++++++++++++-- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 190c880280..d746cf859f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -26,7 +26,7 @@ import fs from 'fs-extra'; import path from 'path'; import nunjucks from 'nunjucks'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; -import { InputError, NotAllowedError } from '@backstage/errors'; +import { InputError, NotAllowedError, stringifyError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; @@ -353,9 +353,22 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { key: string, fn: () => Promise, ) { - const value = await fn(); - task.updateCheckpoint?.(key, value); - return value; + try { + const value = await fn(); + task.updateCheckpoint?.({ + key, + status: 'success', + value, + }); + return value; + } catch (err) { + task.updateCheckpoint?.({ + key, + status: 'failed', + reason: stringifyError(err), + }); + throw err; + } }, createTemporaryDirectory: async () => { const tmpDir = await fs.mkdtemp( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 8762253436..01722edba9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -16,7 +16,11 @@ import { Config } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets, TaskState } from '@backstage/plugin-scaffolder-node'; +import { + TaskSecrets, + TaskState, + UpdateCheckpointOptions, +} from '@backstage/plugin-scaffolder-node'; import { JsonObject, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; @@ -91,8 +95,12 @@ export class TaskManager implements TaskContext { }); } - async updateCheckpoint?(key: string, value: JsonObject): Promise { - this.task.state = { [key]: value }; + async updateCheckpoint?(options: UpdateCheckpointOptions): Promise { + if (this.task.state) { + this.task.state[options.key] = { ...options }; + } else { + this.task.state = { [options.key]: options }; + } await this.storage.saveCheckpoint?.({ taskId: this.task.taskId, state: this.task.state, diff --git a/plugins/scaffolder-backend/src/util/defineCheckpoint.ts b/plugins/scaffolder-backend/src/util/defineCheckpoint.ts index 6656005684..c349bdc7f3 100644 --- a/plugins/scaffolder-backend/src/util/defineCheckpoint.ts +++ b/plugins/scaffolder-backend/src/util/defineCheckpoint.ts @@ -16,15 +16,11 @@ import { JsonObject } from '@backstage/types'; -export type DefineCheckpointProps = { +export const defineCheckpoint = async (props: { checkpoint?: (key: string, fn: () => Promise) => Promise; key: string; fn: () => Promise; -}; - -export const defineCheckpoint = async ( - props: DefineCheckpointProps, -): Promise => { +}): Promise => { const { checkpoint, fn, key } = props; return checkpoint ? checkpoint?.(key, async () => { diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 99638e48af..4e66f1c0e3 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -26,4 +26,5 @@ export type { TaskEventType, TaskState, TaskStatus, + UpdateCheckpointOptions, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 7136211671..f57353c917 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -31,7 +31,14 @@ export type TaskSecrets = Record & { * * @public */ -export type TaskState = Record; +export type TaskState = { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonObject; + }; +}; /** * The status of each step of the Task @@ -108,6 +115,24 @@ export type TaskBrokerDispatchOptions = { createdBy?: string; }; +/** + * The options passed to {@link TaskBroker.updateCheckpoint} + * Parameters to store the result of the executed checkpoint + * + * @public + */ +export type UpdateCheckpointOptions = + | { + key: string; + status: 'success'; + value: JsonObject; + } + | { + key: string; + status: 'failed'; + reason: string; + }; + /** * Task * @@ -126,7 +151,7 @@ export interface TaskContext { emitLog(message: string, logMetadata?: JsonObject): Promise; - updateCheckpoint?(key: string, value: JsonObject): Promise; + updateCheckpoint?(options: UpdateCheckpointOptions): Promise; getWorkspaceName(): Promise; } From 911557bae827cecb5de6cadb76b9d52f6e0f1faf Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 4 Feb 2024 21:09:34 +0100 Subject: [PATCH 010/483] Checkpoint Signed-off-by: bnechyporenko --- packages/backend-next/src/index.ts | 1 + plugins/scaffolder-backend-module-github/package.json | 2 +- .../src/actions/helpers.ts | 11 ++++++----- plugins/scaffolder-backend/src/index.ts | 1 - .../src}/defineCheckpoint.ts | 0 plugins/scaffolder-common/src/index.ts | 2 ++ yarn.lock | 3 ++- 7 files changed, 12 insertions(+), 8 deletions(-) rename plugins/{scaffolder-backend/src/util => scaffolder-common/src}/defineCheckpoint.ts (100%) diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 53a51fcfa7..dd99bab5d7 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -41,6 +41,7 @@ backend.add( backend.add(import('@backstage/plugin-permission-backend/alpha')); backend.add(import('@backstage/plugin-proxy-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); +backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index f48feaaae5..b3f5c856b3 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -37,7 +37,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "@octokit/webhooks": "^10.0.0", diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 87c125eda2..f89440e4d5 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -37,7 +37,7 @@ import { entityRefToName, } from './gitHelpers'; import { JsonObject } from '@backstage/types'; -import { defineCheckpoint } from '@backstage/plugin-scaffolder-backend'; +import { defineCheckpoint } from '@backstage/plugin-scaffolder-common'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -208,11 +208,12 @@ export async function createGithubRepoWithCollaboratorsAndTopics( `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, ); } - return { newRepo }; + return { clone_url: newRepo.clone_url, html_url: newRepo.html_url }; }; - const { newRepo } = await defineCheckpoint<{ - newRepo: { clone_url: string; html_url: string }; + const { clone_url, html_url } = await defineCheckpoint<{ + clone_url: string; + html_url: string; }>({ key: 'v1.task.checkpoint.repo.creation', checkpoint, @@ -345,7 +346,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( } } - return newRepo; + return { clone_url, html_url }; } export async function initRepoPushAndProtect( diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index d59499739e..649a5df233 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -23,6 +23,5 @@ export * from './scaffolder'; export * from './service/router'; export * from './lib'; -export { defineCheckpoint } from './util/defineCheckpoint'; export * from './deprecated'; diff --git a/plugins/scaffolder-backend/src/util/defineCheckpoint.ts b/plugins/scaffolder-common/src/defineCheckpoint.ts similarity index 100% rename from plugins/scaffolder-backend/src/util/defineCheckpoint.ts rename to plugins/scaffolder-common/src/defineCheckpoint.ts diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index 4d9b5e39c6..c0434c3cdb 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -34,3 +34,5 @@ export type { TemplatePermissionsV1beta3, TemplateRecoveryV1beta3, } from './TemplateEntityV1beta3'; + +export { defineCheckpoint } from './defineCheckpoint'; diff --git a/yarn.lock b/yarn.lock index 6ffa8fc541..3020b8cb26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8376,7 +8376,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/types": "workspace:^" "@octokit/webhooks": ^10.0.0 @@ -27101,6 +27101,7 @@ __metadata: "@backstage/plugin-playlist-backend": "workspace:^" "@backstage/plugin-proxy-backend": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-backend-module-github": "workspace:^" "@backstage/plugin-search-backend": "workspace:^" "@backstage/plugin-search-backend-module-catalog": "workspace:^" "@backstage/plugin-search-backend-module-explore": "workspace:^" From 07c2c2e7a793576318aec0c25609c13c765540e4 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 4 Feb 2024 21:30:32 +0100 Subject: [PATCH 011/483] Checkpoint Signed-off-by: bnechyporenko --- packages/backend-next/package.json | 1 + .../src/scaffolder/tasks/DatabaseTaskStore.ts | 14 +++++++++++++- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 8 +++++++- .../src/scaffolder/tasks/StorageTaskBroker.ts | 8 ++++++-- .../src/scaffolder/tasks/types.ts | 6 ++++++ plugins/scaffolder-node/src/tasks/index.ts | 2 +- plugins/scaffolder-node/src/tasks/types.ts | 8 +++++--- 7 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index eca36208a9..aca0acbdd3 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -52,6 +52,7 @@ "@backstage/plugin-playlist-backend": "workspace:^", "@backstage/plugin-proxy-backend": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-backend-module-github": "workspace:^", "@backstage/plugin-search-backend": "workspace:^", "@backstage/plugin-search-backend-module-catalog": "workspace:^", "@backstage/plugin-search-backend-module-explore": "workspace:^", diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index e6083a469f..14535d034c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -38,6 +38,7 @@ import { TaskStatus, TaskEventType, TaskSecrets, + TaskState, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -396,7 +397,18 @@ export class DatabaseTaskStore implements TaskStore { }); } - async saveCheckpoint?(options: TaskStoreStateOptions): Promise { + async listCheckpoints({ + taskId, + }: { + taskId: string; + }): Promise<{ state: TaskState }> { + const state = await this.db('tasks') + .where({ id: taskId }) + .select('state'); + return { state: JSON.stringify(state) as unknown as TaskState }; + } + + async saveCheckpoint(options: TaskStoreStateOptions): Promise { if (options.state) { const serializedState = JSON.stringify(options.state); await this.db('tasks') diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index d746cf859f..407c623793 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -332,6 +332,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; + const prevTaskState = await task.getCheckpoints?.(); for (const iteration of iterations) { if (iteration.each) { @@ -354,7 +355,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { fn: () => Promise, ) { try { - const value = await fn(); + let prevValue: U | undefined; + if (prevTaskState) { + prevValue = prevTaskState.state[key] as unknown as U; + } + + const value = prevValue ? prevValue : await fn(); task.updateCheckpoint?.({ key, status: 'success', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 01722edba9..b74c64d7a3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -19,7 +19,7 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets, TaskState, - UpdateCheckpointOptions, + CheckpointRecord, } from '@backstage/plugin-scaffolder-node'; import { JsonObject, Observable } from '@backstage/types'; import { Logger } from 'winston'; @@ -95,7 +95,11 @@ export class TaskManager implements TaskContext { }); } - async updateCheckpoint?(options: UpdateCheckpointOptions): Promise { + async getCheckpoints?(): Promise<{ state: TaskState } | undefined> { + return this.storage.listCheckpoints?.({ taskId: this.task.taskId }); + } + + async updateCheckpoint?(options: CheckpointRecord): Promise { if (this.task.state) { this.task.state[options.key] = { ...options }; } else { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index b6225d46a9..54c0f19782 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -199,6 +199,12 @@ export interface TaskStore { emitLogEvent(options: TaskStoreEmitOptions): Promise; + listCheckpoints?({ + taskId, + }: { + taskId: string; + }): Promise<{ state: TaskState }>; + saveCheckpoint?(options: TaskStoreStateOptions): Promise; listEvents( diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 4e66f1c0e3..60023bf48e 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -26,5 +26,5 @@ export type { TaskEventType, TaskState, TaskStatus, - UpdateCheckpointOptions, + CheckpointRecord, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index f57353c917..c6654ea642 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -116,12 +116,12 @@ export type TaskBrokerDispatchOptions = { }; /** - * The options passed to {@link TaskBroker.updateCheckpoint} + * The record passed to {@link TaskBroker.updateCheckpoint?} * Parameters to store the result of the executed checkpoint * * @public */ -export type UpdateCheckpointOptions = +export type CheckpointRecord = | { key: string; status: 'success'; @@ -151,7 +151,9 @@ export interface TaskContext { emitLog(message: string, logMetadata?: JsonObject): Promise; - updateCheckpoint?(options: UpdateCheckpointOptions): Promise; + getCheckpoints?(): Promise<{ state: TaskState } | undefined>; + + updateCheckpoint?(options: CheckpointRecord): Promise; getWorkspaceName(): Promise; } From ca7ec6a0cb5f5ccffb0b5a902d403972956bf7cf Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 6 Feb 2024 20:30:27 +0100 Subject: [PATCH 012/483] Checkpoint Signed-off-by: bnechyporenko --- .../tasks/NunjucksWorkflowRunner.ts | 5 ++- plugins/scaffolder-node/src/tasks/types.ts | 36 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 407c623793..e9fc334ea0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -357,7 +357,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { try { let prevValue: U | undefined; if (prevTaskState) { - prevValue = prevTaskState.state[key] as unknown as U; + const prevState = prevTaskState.state[key]; + if (prevState.status === 'success') { + prevValue = prevState.value as U; + } } const value = prevValue ? prevValue : await fn(); diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index c6654ea642..62933ad666 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -26,6 +26,24 @@ export type TaskSecrets = Record & { backstageToken?: string; }; +/** + * The record passed to {@link TaskBroker.updateCheckpoint?} + * Parameters to store the result of the executed checkpoint + * + * @public + */ +export type CheckpointRecord = + | { + key: string; + status: 'success'; + value: JsonObject; + } + | { + key: string; + status: 'failed'; + reason: string; + }; + /** * TaskState * @@ -115,24 +133,6 @@ export type TaskBrokerDispatchOptions = { createdBy?: string; }; -/** - * The record passed to {@link TaskBroker.updateCheckpoint?} - * Parameters to store the result of the executed checkpoint - * - * @public - */ -export type CheckpointRecord = - | { - key: string; - status: 'success'; - value: JsonObject; - } - | { - key: string; - status: 'failed'; - reason: string; - }; - /** * Task * From c6b132e7d933ed2c3bd25353ffb9d8b6817ec017 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 6 Feb 2024 20:41:26 +0100 Subject: [PATCH 013/483] wip Signed-off-by: bnechyporenko --- .changeset/sixty-queens-mix.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/sixty-queens-mix.md diff --git a/.changeset/sixty-queens-mix.md b/.changeset/sixty-queens-mix.md new file mode 100644 index 0000000000..52f2ca5c39 --- /dev/null +++ b/.changeset/sixty-queens-mix.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-backend-module-github': patch +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Introducing checkpoints for scaffolder task action idempotency From 72d7c6867ab30512ba7609fc9f6609f52f0d4dca Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 6 Feb 2024 20:49:08 +0100 Subject: [PATCH 014/483] wip Signed-off-by: bnechyporenko --- .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 940e855828..46b6e9b7d6 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -158,6 +158,7 @@ hotspots http https Iain +idempotency Iglesias iLert img From d8c7e5eb6502d03ef79f9f085351fba975cdf0c9 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Wed, 7 Feb 2024 12:30:04 +0000 Subject: [PATCH 015/483] feat: adds last commit and its status to the github-pull-requests-board Signed-off-by: Josh Uvi --- packages/app/package.json | 1 + packages/app/src/components/catalog/EntityPage.tsx | 4 ++++ .../src/api/useGetPullRequestDetails.ts | 9 +++++++++ .../src/components/Card/Card.tsx | 5 ++++- .../src/components/Card/CardHeader.tsx | 10 +++++++++- .../EntityTeamPullRequestsContent.tsx | 5 +++++ .../components/PullRequestCard/PullRequestCard.tsx | 5 ++++- .../github-pull-requests-board/src/utils/types.tsx | 11 +++++++++++ yarn.lock | 3 ++- 9 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 43b12a1cb7..769a28eec1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -45,6 +45,7 @@ "@backstage/plugin-gcalendar": "workspace:^", "@backstage/plugin-gcp-projects": "workspace:^", "@backstage/plugin-github-actions": "workspace:^", + "@backstage/plugin-github-pull-requests-board": "workspace:^", "@backstage/plugin-gocd": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", "@backstage/plugin-home": "workspace:^", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 4d5f65cbbe..b0b227be92 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -185,6 +185,7 @@ import { isLinguistAvailable, EntityLinguistCard, } from '@backstage/plugin-linguist'; +import { EntityTeamPullRequestsContent } from '@backstage/plugin-github-pull-requests-board'; const customEntityFilterKind = ['Component', 'API', 'System']; @@ -809,6 +810,9 @@ const groupPage = ( + + + ); diff --git a/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts index 75f4d37cf4..8b919ce3d6 100644 --- a/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts +++ b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts @@ -57,6 +57,15 @@ export const useGetPullRequestDetails = () => { state } } + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } mergeable state reviewDecision diff --git a/plugins/github-pull-requests-board/src/components/Card/Card.tsx b/plugins/github-pull-requests-board/src/components/Card/Card.tsx index 4fe833fa67..09c6352b60 100644 --- a/plugins/github-pull-requests-board/src/components/Card/Card.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/Card.tsx @@ -16,7 +16,7 @@ import React, { PropsWithChildren, FunctionComponent } from 'react'; import { Box, Paper, CardActionArea } from '@material-ui/core'; import CardHeader from './CardHeader'; -import { Label } from '../../utils/types'; +import { Label, Status } from '../../utils/types'; type Props = { title: string; @@ -29,6 +29,7 @@ type Props = { isDraft: boolean; repositoryIsArchived: boolean; labels?: Label[]; + status: Status; }; const Card: FunctionComponent> = ( @@ -45,6 +46,7 @@ const Card: FunctionComponent> = ( isDraft, repositoryIsArchived, labels, + status, children, } = props; @@ -63,6 +65,7 @@ const Card: FunctionComponent> = ( isDraft={isDraft} repositoryIsArchived={repositoryIsArchived} labels={labels} + status={status} /> {children} diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx index c59349b95f..98d53f781c 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx @@ -19,7 +19,7 @@ import { getElapsedTime } from '../../utils/functions'; import { UserHeader } from '../UserHeader'; import { DraftPrIcon } from '../icons/DraftPr'; import UnarchiveIcon from '@material-ui/icons/Unarchive'; -import { Label } from '../../utils/types'; +import { Label, Status } from '../../utils/types'; import { useFormClasses } from './styles'; type Props = { @@ -32,6 +32,7 @@ type Props = { isDraft: boolean; repositoryIsArchived: boolean; labels?: Label[]; + status: Status; }; const CardHeader: FunctionComponent = (props: Props) => { @@ -47,6 +48,7 @@ const CardHeader: FunctionComponent = (props: Props) => { isDraft, repositoryIsArchived, labels, + status: commitStatus, } = props; return ( @@ -86,6 +88,12 @@ const CardHeader: FunctionComponent = (props: Props) => { )} + + + Commit Status:{' '} + {commitStatus.commit.statusCheckRollup.state} + + {labels && ( {labels.map(data => { diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx index f901acb119..6109ddb63c 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx @@ -87,6 +87,9 @@ const EntityTeamPullRequestsContent = ( return ; } + // eslint-disable-next-line no-console + console.log('pull - ', pullRequests); + return ( {pullRequests.length ? ( @@ -103,6 +106,7 @@ const EntityTeamPullRequestsContent = ( author, url, latestReviews, + commits, repository, isDraft, labels, @@ -125,6 +129,7 @@ const EntityTeamPullRequestsContent = ( author={author} url={url} reviews={latestReviews.nodes} + status={commits.nodes} repositoryName={repository.name} repositoryIsArchived={repository.isArchived} isDraft={isDraft} diff --git a/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx index e9ad2aeab6..8736644f09 100644 --- a/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx +++ b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx @@ -19,7 +19,7 @@ import { getChangeRequests, getCommentedReviews, } from '../../utils/functions'; -import { Reviews, Author, Label } from '../../utils/types'; +import { Reviews, Author, Label, Status } from '../../utils/types'; import { Card } from '../Card'; import { UserHeaderList } from '../UserHeaderList'; @@ -30,6 +30,7 @@ type Props = { author: Author; url: string; reviews: Reviews; + status: Status; repositoryName: string; repositoryIsArchived: boolean; isDraft: boolean; @@ -44,6 +45,7 @@ const PullRequestCard: FunctionComponent = (props: Props) => { author, url, reviews, + status, repositoryName, repositoryIsArchived, isDraft, @@ -66,6 +68,7 @@ const PullRequestCard: FunctionComponent = (props: Props) => { isDraft={isDraft} repositoryIsArchived={repositoryIsArchived} labels={labels} + status={status} > {!!approvedReviews.length && ( Date: Wed, 7 Feb 2024 11:43:08 +0000 Subject: [PATCH 016/483] Ability to fetch the README file from a different AZD path, using an annotation on the entity Signed-off-by: David Roberts --- .../azure-devops-backend/src/api/AzureDevOpsApi.ts | 3 ++- plugins/azure-devops-backend/src/service/router.ts | 2 ++ plugins/azure-devops-common/src/constants.ts | 2 ++ plugins/azure-devops-common/src/types.ts | 1 + plugins/azure-devops/src/api/AzureDevOpsClient.ts | 3 +++ plugins/azure-devops/src/hooks/useReadme.ts | 11 +++++++++-- .../src/utils/getAnnotationValuesFromEntity.ts | 7 +++++++ 7 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index d50888006b..cc5cad92b7 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -527,11 +527,12 @@ export class AzureDevOpsApi { org: string, project: string, repo: string, + path: string, ): Promise<{ url: string; content: string; }> { - const url = buildEncodedUrl(host, org, project, repo, 'README.md'); + const url = buildEncodedUrl(host, org, project, repo, path); const response = await this.urlReader.readUrl(url); const buffer = await response.buffer(); const content = await replaceReadme( diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 9a71c8f03d..a00b0ce910 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -216,12 +216,14 @@ export async function createRouter( req.query.host?.toString() ?? config.getString('azureDevOps.host'); const org = req.query.org?.toString() ?? config.getString('azureDevOps.organization'); + const path = req.query.path?.toString() ?? 'README.md'; const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, org, projectName, repoName, + path, ); res.status(200).json(readme); }); diff --git a/plugins/azure-devops-common/src/constants.ts b/plugins/azure-devops-common/src/constants.ts index 5cc6b54592..05eb430cc9 100644 --- a/plugins/azure-devops-common/src/constants.ts +++ b/plugins/azure-devops-common/src/constants.ts @@ -22,6 +22,8 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; /** @public */ export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; /** @public */ +export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; +/** @public */ export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; /** @public */ export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index afb6083867..0390bb1f6e 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -212,6 +212,7 @@ export interface ReadmeConfig { repo: string; host?: string; org?: string; + path?: string; } /** @public */ diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 76e38b064c..a10b10ff14 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -189,6 +189,9 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (opts.org) { queryString.append('org', opts.org); } + if (opts.path) { + queryString.append('path', opts.path); + } return await this.get( `readme/${encodeURIComponent(opts.project)}/${encodeURIComponent( opts.repo, diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts index 348b4219f0..9aa376e0bd 100644 --- a/plugins/azure-devops/src/hooks/useReadme.ts +++ b/plugins/azure-devops/src/hooks/useReadme.ts @@ -30,8 +30,15 @@ export function useReadme(entity: Entity): { const api = useApi(azureDevOpsApiRef); const { value, loading, error } = useAsync(() => { - const { project, repo, host, org } = getAnnotationValuesFromEntity(entity); - return api.getReadme({ project, repo: repo as string, host, org }); + const { project, repo, host, org, readmePath } = + getAnnotationValuesFromEntity(entity); + return api.getReadme({ + project, + repo: repo as string, + host, + org, + path: readmePath, + }); }, [api]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 7533c473ed..fb38a531e1 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, + AZURE_DEVOPS_README_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, AZURE_DEVOPS_HOST_ORG_ANNOTATION, } from '@backstage/plugin-azure-devops-common'; @@ -28,6 +29,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { definition?: string; host?: string; org?: string; + readmePath?: string; } { const hostOrg = getHostOrg(entity.metadata.annotations); const projectRepo = getProjectRepo(entity.metadata.annotations); @@ -35,12 +37,15 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; const definition = entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; + const readmePath = + entity.metadata.annotations?.[AZURE_DEVOPS_README_ANNOTATION]; if (definition) { if (project) { return { project, definition, + readmePath: readmePath, ...hostOrg, }; } @@ -49,6 +54,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { project: projectRepo.project, repo: projectRepo.repo, definition, + readmePath: readmePath, ...hostOrg, }; } @@ -60,6 +66,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { return { project: projectRepo.project, repo: projectRepo.repo, + readmePath: readmePath, ...hostOrg, }; } From 9fdb86a91f5d24ad9e12b756dcf53eb614c8a7a4 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 11:56:28 +0000 Subject: [PATCH 017/483] Document the changes Signed-off-by: David Roberts --- .changeset/itchy-news-drive.md | 15 +++++++++++++++ plugins/azure-devops/README.md | 10 +++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-news-drive.md diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md new file mode 100644 index 0000000000..b5bcdaec73 --- /dev/null +++ b/.changeset/itchy-news-drive.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-azure-devops-common': minor +'@backstage/plugin-azure-devops': minor +--- + +Ability to fetch the README file from a different AZD path. + +Defaults to the current, AZD default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + +Example: + +```yaml +dev.azure.com/readme-path: /my-path/CHANGELOG.md +``` diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index f897588987..c1191b1566 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -63,13 +63,21 @@ spec: #### Mono repos -If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity. +If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity: ```yaml dev.azure.com/project-repo: / dev.azure.com/build-definition: ``` +...and which README file belongs to each entity. + +Example: + +```yaml +dev.azure.com/readme-path: //.md +``` + #### Pipeline in different project to repo If your pipeline is in a different project to the source code, you will need to specify this in the project annotation. From 0081922e4710095ca156606a0c7f4cb6db3c410b Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 12:11:16 +0000 Subject: [PATCH 018/483] Update the API reports Signed-off-by: David Roberts --- plugins/azure-devops-backend/api-report.md | 1 + plugins/azure-devops-common/api-report.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 6dead7f11b..d98d178cff 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -124,6 +124,7 @@ export class AzureDevOpsApi { org: string, project: string, repo: string, + path: string, ): Promise<{ url: string; content: string; diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 5bd1674fc8..e8343178e3 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -16,6 +16,9 @@ export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; // @public (undocumented) export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; +// @public (undocumented) +export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; + // @public (undocumented) export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; @@ -228,6 +231,8 @@ export interface ReadmeConfig { // (undocumented) org?: string; // (undocumented) + path?: string; + // (undocumented) project: string; // (undocumented) repo: string; From ded6e4df61f1b6b367248d15417f039321409cbb Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Feb 2024 13:39:01 +0000 Subject: [PATCH 019/483] Update the tests to reflect the new parameter and default Signed-off-by: David Roberts --- .../src/service/router.test.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 65cb27271e..9e8f968826 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -482,7 +482,7 @@ describe('createRouter', () => { }); describe('GET /readme/:projectName/:repoName', () => { - it('fetches readme file', async () => { + it('fetches default readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`; @@ -491,14 +491,41 @@ describe('createRouter', () => { url, }); + const response = await request(app).get('/readme/myProject/myRepo'); + expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( + 'host.com', + 'myOrg', + 'myProject', + 'myRepo', + 'README.md', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + content, + url, + }); + }); + }); + + describe('GET /readme/:projectName/:repoName with readme path', () => { + it('fetches specified readme file', async () => { + const content = getReadmeMock(); + const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README_NOT_DEFAULT.md`; + + azureDevOpsApi.getReadme.mockResolvedValueOnce({ + content, + url, + }); + const response = await request(app).get( - '/readme/myProject/myRepo?path=README.md', + '/readme/myProject/myRepo?path=README_NOT_DEFAULT.md', ); expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( 'host.com', 'myOrg', 'myProject', 'myRepo', + 'README_NOT_DEFAULT.md', ); expect(response.status).toEqual(200); expect(response.body).toEqual({ From 92582f17a011e39b647134d4dc127115fb54ea67 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Thu, 8 Feb 2024 20:43:27 +0100 Subject: [PATCH 020/483] wip Signed-off-by: bnechyporenko --- .../src/actions/github.ts | 1 - .../src/actions/helpers.ts | 172 +++++++----------- .../tasks/NunjucksWorkflowRunner.ts | 5 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 1 + .../scaffolder-common/src/defineCheckpoint.ts | 30 --- plugins/scaffolder-common/src/index.ts | 2 - plugins/scaffolder-node/src/actions/types.ts | 4 +- plugins/scaffolder-node/src/tasks/types.ts | 6 +- 8 files changed, 74 insertions(+), 147 deletions(-) delete mode 100644 plugins/scaffolder-common/src/defineCheckpoint.ts diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index ed55714852..91d7a34aea 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -244,7 +244,6 @@ export function createPublishGithubAction(options: { repoVariables, secrets, ctx.logger, - ctx.checkpoint, ); const remoteUrl = 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 f89440e4d5..4d95a6494f 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -36,8 +36,6 @@ import { enableBranchProtectionOnDefaultRepoBranch, entityRefToName, } from './gitHelpers'; -import { JsonObject } from '@backstage/types'; -import { defineCheckpoint } from '@backstage/plugin-scaffolder-common'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -140,10 +138,6 @@ export async function createGithubRepoWithCollaboratorsAndTopics( repoVariables: { [key: string]: string } | undefined, secrets: { [key: string]: string } | undefined, logger: Logger, - checkpoint?: ( - key: string, - fn: () => Promise, - ) => Promise, ) { // eslint-disable-next-line testing-library/no-await-sync-queries const user = await client.rest.users.getByUsername({ @@ -154,71 +148,59 @@ export async function createGithubRepoWithCollaboratorsAndTopics( await validateAccessTeam(client, access); } - const repoCreation = async () => { - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - private: repoVisibility === 'private', - // @ts-ignore https://github.com/octokit/types.ts/issues/522 - visibility: repoVisibility, - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - squash_merge_commit_title: squashMergeCommitTitle, - squash_merge_commit_message: squashMergeCommitMessage, - allow_rebase_merge: allowRebaseMerge, - allow_auto_merge: allowAutoMerge, - homepage: homepage, - has_projects: hasProjects, - has_wiki: hasWiki, - has_issues: hasIssues, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - private: repoVisibility === 'private', - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - squash_merge_commit_title: squashMergeCommitTitle, - squash_merge_commit_message: squashMergeCommitMessage, - allow_rebase_merge: allowRebaseMerge, - allow_auto_merge: allowAutoMerge, - homepage: homepage, - has_projects: hasProjects, - has_wiki: hasWiki, - has_issues: hasIssues, - }); + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + private: repoVisibility === 'private', + // @ts-ignore https://github.com/octokit/types.ts/issues/522 + visibility: repoVisibility, + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + squash_merge_commit_title: squashMergeCommitTitle, + squash_merge_commit_message: squashMergeCommitMessage, + allow_rebase_merge: allowRebaseMerge, + allow_auto_merge: allowAutoMerge, + homepage: homepage, + has_projects: hasProjects, + has_wiki: hasWiki, + has_issues: hasIssues, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + private: repoVisibility === 'private', + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + squash_merge_commit_title: squashMergeCommitTitle, + squash_merge_commit_message: squashMergeCommitMessage, + allow_rebase_merge: allowRebaseMerge, + allow_auto_merge: allowAutoMerge, + homepage: homepage, + has_projects: hasProjects, + has_wiki: hasWiki, + has_issues: hasIssues, + }); - let newRepo; + let newRepo; - try { - newRepo = (await repoCreationPromise).data; - } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { - logger.warn( - `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, - ); - } - throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + try { + newRepo = (await repoCreationPromise).data; + } catch (e) { + assertError(e); + if (e.message === 'Resource not accessible by integration') { + logger.warn( + `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, ); } - return { clone_url: newRepo.clone_url, html_url: newRepo.html_url }; - }; - - const { clone_url, html_url } = await defineCheckpoint<{ - clone_url: string; - html_url: string; - }>({ - key: 'v1.task.checkpoint.repo.creation', - checkpoint, - fn: repoCreation, - }); + throw new Error( + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + ); + } if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); @@ -231,19 +213,11 @@ export async function createGithubRepoWithCollaboratorsAndTopics( }); // No need to add access if it's the person who owns the personal account } else if (access && access !== owner) { - const addCollaborator = async () => { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: access, - permission: 'admin', - }); - return {}; - }; - await defineCheckpoint({ - key: 'v1.task.checkpoint.add.collaborator', - checkpoint, - fn: addCollaborator, + await client.rest.repos.addCollaborator({ + owner, + repo, + username: access, + permission: 'admin', }); } @@ -251,19 +225,11 @@ export async function createGithubRepoWithCollaboratorsAndTopics( for (const collaborator of collaborators) { try { if ('user' in collaborator) { - const addCollaborator = async () => { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: entityRefToName(collaborator.user), - permission: collaborator.access, - }); - return {}; - }; - await defineCheckpoint({ - key: `v1.task.checkpoint.add.collaborator.${collaborator.user}`, - checkpoint, - fn: addCollaborator, + await client.rest.repos.addCollaborator({ + owner, + repo, + username: entityRefToName(collaborator.user), + permission: collaborator.access, }); } else if ('team' in collaborator) { await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ @@ -298,19 +264,11 @@ export async function createGithubRepoWithCollaboratorsAndTopics( } for (const [key, value] of Object.entries(repoVariables ?? {})) { - const createRepoVariable = async () => { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, - }); - return {}; - }; - await defineCheckpoint({ - key: `v1.task.checkpoint.create.repo.variable.${key}`, - checkpoint, - fn: createRepoVariable, + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, }); } @@ -346,7 +304,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( } } - return { clone_url, html_url }; + return newRepo; } export async function initRepoPushAndProtect( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index e9fc334ea0..d9c1979eee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -350,10 +350,11 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { logger: taskLogger, logStream: streamLogger, workspacePath, - async checkpoint( - key: string, + async checkpoint( + keySuffix: string, fn: () => Promise, ) { + const key = `v1.task.checkpoint.${keySuffix}`; try { let prevValue: U | undefined; if (prevTaskState) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index b74c64d7a3..d7fe1be6af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -100,6 +100,7 @@ export class TaskManager implements TaskContext { } async updateCheckpoint?(options: CheckpointRecord): Promise { + // drop the key if (this.task.state) { this.task.state[options.key] = { ...options }; } else { diff --git a/plugins/scaffolder-common/src/defineCheckpoint.ts b/plugins/scaffolder-common/src/defineCheckpoint.ts deleted file mode 100644 index c349bdc7f3..0000000000 --- a/plugins/scaffolder-common/src/defineCheckpoint.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { JsonObject } from '@backstage/types'; - -export const defineCheckpoint = async (props: { - checkpoint?: (key: string, fn: () => Promise) => Promise; - key: string; - fn: () => Promise; -}): Promise => { - const { checkpoint, fn, key } = props; - return checkpoint - ? checkpoint?.(key, async () => { - return await fn(); - }) - : fn(); -}; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index c0434c3cdb..4d9b5e39c6 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -34,5 +34,3 @@ export type { TemplatePermissionsV1beta3, TemplateRecoveryV1beta3, } from './TemplateEntityV1beta3'; - -export { defineCheckpoint } from './defineCheckpoint'; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index a7f44c4730..5abeb29a2b 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -16,7 +16,7 @@ import { Logger } from 'winston'; import { Writable } from 'stream'; -import { JsonObject } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { TaskSecrets } from '../tasks'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UserEntity } from '@backstage/catalog-model'; @@ -35,7 +35,7 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint?( + checkpoint( key: string, fn: () => Promise, ): Promise; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 62933ad666..b2bff61e03 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -15,7 +15,7 @@ */ import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { JsonObject, Observable } from '@backstage/types'; +import { JsonObject, JsonValue, Observable } from '@backstage/types'; /** * TaskSecrets @@ -36,7 +36,7 @@ export type CheckpointRecord = | { key: string; status: 'success'; - value: JsonObject; + value: JsonValue; } | { key: string; @@ -54,7 +54,7 @@ export type TaskState = { | { status: 'failed'; reason: string } | { status: 'success'; - value: JsonObject; + value: JsonValue; }; }; From a2ee37bc6d88f1e7bda14d4ea79f68e8e59c6fa4 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Thu, 8 Feb 2024 21:20:16 +0100 Subject: [PATCH 021/483] wip Signed-off-by: bnechyporenko --- plugins/scaffolder-backend/api-report.md | 30 ++++++++++++++ .../src/scaffolder/tasks/StorageTaskBroker.ts | 6 +-- .../src/scaffolder/tasks/index.ts | 1 + .../src/scaffolder/tasks/types.ts | 5 +++ plugins/scaffolder-node/api-report.md | 41 +++++++++++++++++++ plugins/scaffolder-node/src/actions/types.ts | 2 +- plugins/scaffolder-node/src/tasks/types.ts | 4 +- 7 files changed, 83 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 467887dfc5..742834a4df 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -9,6 +9,7 @@ import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucke import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; import { CatalogApi } from '@backstage/catalog-client'; +import { CheckpointRecord } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin-scaffolder-node'; @@ -47,6 +48,7 @@ import { TaskRecovery } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TaskState } from '@backstage/plugin-scaffolder-node'; import { TaskStatus as TaskStatus_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node'; @@ -359,6 +361,7 @@ export interface CurrentClaimedTask { createdBy?: string; secrets?: TaskSecrets_2; spec: TaskSpec; + state?: TaskState; taskId: string; } @@ -403,6 +406,10 @@ export class DatabaseTaskStore implements TaskStore { tasks: SerializedTask_2[]; }>; // (undocumented) + listCheckpoints({ taskId }: { taskId: string }): Promise<{ + state: TaskState; + }>; + // (undocumented) listEvents(options: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent_2[]; }>; @@ -418,6 +425,8 @@ export class DatabaseTaskStore implements TaskStore { ids: string[]; }>; // (undocumented) + saveCheckpoint(options: TaskStoreStateOptions): Promise; + // (undocumented) shutdownTask(options: TaskStoreShutDownTaskOptions): Promise; } @@ -519,11 +528,20 @@ export class TaskManager implements TaskContext { // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) + getCheckpoints?(): Promise< + | { + state: TaskState; + } + | undefined + >; + // (undocumented) getWorkspaceName(): Promise; // (undocumented) get secrets(): TaskSecrets_2 | undefined; // (undocumented) get spec(): TaskSpecV1beta3; + // (undocumented) + updateCheckpoint?(options: CheckpointRecord): Promise; } // @public @deprecated (undocumented) @@ -559,6 +577,10 @@ export interface TaskStore { tasks: SerializedTask[]; }>; // (undocumented) + listCheckpoints?({ taskId }: { taskId: string }): Promise<{ + state: TaskState; + }>; + // (undocumented) listEvents(options: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; }>; @@ -573,6 +595,8 @@ export interface TaskStore { ids: string[]; }>; // (undocumented) + saveCheckpoint?(options: TaskStoreStateOptions): Promise; + // (undocumented) shutdownTask?(options: TaskStoreShutDownTaskOptions): Promise; } @@ -610,6 +634,12 @@ export type TaskStoreShutDownTaskOptions = { taskId: string; }; +// @public +export type TaskStoreStateOptions = { + taskId: string; + state?: TaskState; +}; + // @public export class TaskWorker { // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index d7fe1be6af..6e01141dab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -100,11 +100,11 @@ export class TaskManager implements TaskContext { } async updateCheckpoint?(options: CheckpointRecord): Promise { - // drop the key + const { key, ...value } = options; if (this.task.state) { - this.task.state[options.key] = { ...options }; + this.task.state[key] = value; } else { - this.task.state = { [options.key]: options }; + this.task.state = { [key]: value }; } await this.storage.saveCheckpoint?.({ taskId: this.task.taskId, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 9d231d7e7c..2da812e869 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -25,6 +25,7 @@ export type { TaskStoreEmitOptions, TaskStoreListEventsOptions, TaskStoreShutDownTaskOptions, + TaskStoreStateOptions, SerializedTask, SerializedTaskEvent, TaskStatus, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 54c0f19782..e30de5db94 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -113,6 +113,11 @@ export type TaskStoreEmitOptions = { body: TBody; }; +/** + * TaskStoreStateOptions + * + * @public + */ export type TaskStoreStateOptions = { taskId: string; state?: TaskState; diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index fec2205462..d3bc9d8500 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -30,6 +30,10 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; + checkpoint?( + key: string, + fn: () => Promise, + ): Promise; output( name: keyof TActionOutput, value: TActionOutput[keyof TActionOutput], @@ -60,6 +64,19 @@ export function addFiles(options: { logger?: Logger | undefined; }): Promise; +// @public +export type CheckpointRecord = + | { + key: string; + status: 'success'; + value: JsonValue; + } + | { + key: string; + status: 'failed'; + reason: string; + }; + // @public (undocumented) export function cloneRepo(options: { url: string; @@ -345,6 +362,13 @@ export interface TaskContext { // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) + getCheckpoints?(): Promise< + | { + state: TaskState; + } + | undefined + >; + // (undocumented) getWorkspaceName(): Promise; // (undocumented) isDryRun?: boolean; @@ -352,6 +376,10 @@ export interface TaskContext { secrets?: TaskSecrets; // (undocumented) spec: TaskSpec; + // (undocumented) + state?: TaskState; + // (undocumented) + updateCheckpoint?(options: CheckpointRecord): Promise; } // @public @@ -362,6 +390,19 @@ export type TaskSecrets = Record & { backstageToken?: string; }; +// @public +export type TaskState = { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; +}; + // @public export type TaskStatus = | 'cancelled' diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 5abeb29a2b..678d6f87d7 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -35,7 +35,7 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint( + checkpoint?( key: string, fn: () => Promise, ): Promise; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index b2bff61e03..a7fbf4ac76 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -27,7 +27,7 @@ export type TaskSecrets = Record & { }; /** - * The record passed to {@link TaskBroker.updateCheckpoint?} + * The record passed to TaskBroker for updating a checkpoint. * Parameters to store the result of the executed checkpoint * * @public @@ -45,7 +45,7 @@ export type CheckpointRecord = }; /** - * TaskState + * The state of all task's checkpoints * * @public */ From 2b900fee6dcd956bff1503bf89a602d45a7a195e Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sat, 10 Feb 2024 14:34:14 +0100 Subject: [PATCH 022/483] wip Signed-off-by: bnechyporenko --- plugins/scaffolder-backend/api-report.md | 123 ++++++++++++++---- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 51 ++++++-- .../tasks/NunjucksWorkflowRunner.ts | 2 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 47 +++++-- .../src/scaffolder/tasks/index.ts | 1 - .../src/scaffolder/tasks/types.ts | 42 +++--- plugins/scaffolder-node/api-report.md | 53 +++++--- plugins/scaffolder-node/src/tasks/index.ts | 1 - plugins/scaffolder-node/src/tasks/types.ts | 55 +++++--- 9 files changed, 274 insertions(+), 101 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 742834a4df..b226372d86 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -9,7 +9,6 @@ import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucke import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; import { CatalogApi } from '@backstage/catalog-client'; -import { CheckpointRecord } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin-scaffolder-node'; @@ -22,6 +21,7 @@ import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; @@ -48,7 +48,6 @@ import { TaskRecovery } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TaskState } from '@backstage/plugin-scaffolder-node'; import { TaskStatus as TaskStatus_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node'; @@ -361,7 +360,17 @@ export interface CurrentClaimedTask { createdBy?: string; secrets?: TaskSecrets_2; spec: TaskSpec; - state?: TaskState; + state?: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; taskId: string; } @@ -400,16 +409,29 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) getTask(taskId: string): Promise; // (undocumented) + getTaskState({ taskId }: { taskId: string }): Promise< + | { + state: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + >; + // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) list(options: { createdBy?: string }): Promise<{ tasks: SerializedTask_2[]; }>; // (undocumented) - listCheckpoints({ taskId }: { taskId: string }): Promise<{ - state: TaskState; - }>; - // (undocumented) listEvents(options: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent_2[]; }>; @@ -425,7 +447,22 @@ export class DatabaseTaskStore implements TaskStore { ids: string[]; }>; // (undocumented) - saveCheckpoint(options: TaskStoreStateOptions): Promise; + saveCheckpoint(options: { + taskId: string; + state?: + | { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + } + | undefined; + }): Promise; // (undocumented) shutdownTask(options: TaskStoreShutDownTaskOptions): Promise; } @@ -528,9 +565,19 @@ export class TaskManager implements TaskContext { // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) - getCheckpoints?(): Promise< + getTaskState?(): Promise< | { - state: TaskState; + state: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; } | undefined >; @@ -541,7 +588,19 @@ export class TaskManager implements TaskContext { // (undocumented) get spec(): TaskSpecV1beta3; // (undocumented) - updateCheckpoint?(options: CheckpointRecord): Promise; + updateCheckpoint?( + options: + | { + key: string; + status: 'success'; + value: JsonValue; + } + | { + key: string; + status: 'failed'; + reason: string; + }, + ): Promise; } // @public @deprecated (undocumented) @@ -571,16 +630,29 @@ export interface TaskStore { // (undocumented) getTask(taskId: string): Promise; // (undocumented) + getTaskState?({ taskId }: { taskId: string }): Promise< + | { + state: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + >; + // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) - listCheckpoints?({ taskId }: { taskId: string }): Promise<{ - state: TaskState; - }>; - // (undocumented) listEvents(options: TaskStoreListEventsOptions): Promise<{ events: SerializedTaskEvent[]; }>; @@ -595,7 +667,20 @@ export interface TaskStore { ids: string[]; }>; // (undocumented) - saveCheckpoint?(options: TaskStoreStateOptions): Promise; + saveCheckpoint?(options: { + taskId: string; + state?: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; + }): Promise; // (undocumented) shutdownTask?(options: TaskStoreShutDownTaskOptions): Promise; } @@ -634,12 +719,6 @@ export type TaskStoreShutDownTaskOptions = { taskId: string; }; -// @public -export type TaskStoreStateOptions = { - taskId: string; - state?: TaskState; -}; - // @public export class TaskWorker { // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 14535d034c..0d6521dfd2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { PluginDatabaseManager, resolvePackagePath, @@ -30,7 +30,6 @@ import { TaskStoreCreateTaskResult, TaskStoreShutDownTaskOptions, TaskStoreRecoverTaskOptions, - TaskStoreStateOptions, } from './types'; import { SerializedTaskEvent, @@ -38,7 +37,6 @@ import { TaskStatus, TaskEventType, TaskSecrets, - TaskState, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -397,18 +395,49 @@ export class DatabaseTaskStore implements TaskStore { }); } - async listCheckpoints({ - taskId, - }: { - taskId: string; - }): Promise<{ state: TaskState }> { - const state = await this.db('tasks') + async getTaskState({ taskId }: { taskId: string }): Promise< + | { + state: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + > { + const [result] = await this.db('tasks') .where({ id: taskId }) .select('state'); - return { state: JSON.stringify(state) as unknown as TaskState }; + return result.state + ? { + state: JSON.parse(result.state) as unknown as { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }, + } + : undefined; } - async saveCheckpoint(options: TaskStoreStateOptions): Promise { + async saveCheckpoint(options: { + taskId: string; + state?: + | { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + } + | undefined; + }): Promise { if (options.state) { const serializedState = JSON.stringify(options.state); await this.db('tasks') diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index d9c1979eee..b5ffc73189 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -332,7 +332,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; - const prevTaskState = await task.getCheckpoints?.(); + const prevTaskState = await task.getTaskState?.(); for (const iteration of iterations) { if (iteration.each) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 6e01141dab..453df09c77 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -16,12 +16,8 @@ import { Config } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { - TaskSecrets, - TaskState, - CheckpointRecord, -} from '@backstage/plugin-scaffolder-node'; -import { JsonObject, Observable } from '@backstage/types'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; import { @@ -95,11 +91,35 @@ export class TaskManager implements TaskContext { }); } - async getCheckpoints?(): Promise<{ state: TaskState } | undefined> { - return this.storage.listCheckpoints?.({ taskId: this.task.taskId }); + async getTaskState?(): Promise< + | { + state: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + > { + return this.storage.getTaskState?.({ taskId: this.task.taskId }); } - async updateCheckpoint?(options: CheckpointRecord): Promise { + async updateCheckpoint?( + options: + | { + key: string; + status: 'success'; + value: JsonValue; + } + | { + key: string; + status: 'failed'; + reason: string; + }, + ): Promise { const { key, ...value } = options; if (this.task.state) { this.task.state[key] = value; @@ -168,7 +188,14 @@ export interface CurrentClaimedTask { /** * The state of checkpoints of the task. */ - state?: TaskState; + state?: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; /** * The creator of the task. */ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index 2da812e869..9d231d7e7c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -25,7 +25,6 @@ export type { TaskStoreEmitOptions, TaskStoreListEventsOptions, TaskStoreShutDownTaskOptions, - TaskStoreStateOptions, SerializedTask, SerializedTaskEvent, TaskStatus, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index e30de5db94..215defd280 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -16,7 +16,7 @@ import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets, TaskState } from '@backstage/plugin-scaffolder-node'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { TemplateAction, TaskStatus as _TaskStatus, @@ -113,16 +113,6 @@ export type TaskStoreEmitOptions = { body: TBody; }; -/** - * TaskStoreStateOptions - * - * @public - */ -export type TaskStoreStateOptions = { - taskId: string; - state?: TaskState; -}; - /** * TaskStoreListEventsOptions * @@ -204,13 +194,31 @@ export interface TaskStore { emitLogEvent(options: TaskStoreEmitOptions): Promise; - listCheckpoints?({ - taskId, - }: { - taskId: string; - }): Promise<{ state: TaskState }>; + getTaskState?({ taskId }: { taskId: string }): Promise< + | { + state: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + >; - saveCheckpoint?(options: TaskStoreStateOptions): Promise; + saveCheckpoint?(options: { + taskId: string; + state?: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; + }): Promise; listEvents( options: TaskStoreListEventsOptions, diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index d3bc9d8500..c87ec8183c 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -64,19 +64,6 @@ export function addFiles(options: { logger?: Logger | undefined; }): Promise; -// @public -export type CheckpointRecord = - | { - key: string; - status: 'success'; - value: JsonValue; - } - | { - key: string; - status: 'failed'; - reason: string; - }; - // @public (undocumented) export function cloneRepo(options: { url: string; @@ -362,9 +349,19 @@ export interface TaskContext { // (undocumented) emitLog(message: string, logMetadata?: JsonObject): Promise; // (undocumented) - getCheckpoints?(): Promise< + getTaskState?(): Promise< | { - state: TaskState; + state: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; } | undefined >; @@ -377,9 +374,31 @@ export interface TaskContext { // (undocumented) spec: TaskSpec; // (undocumented) - state?: TaskState; + state?: { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; + }; // (undocumented) - updateCheckpoint?(options: CheckpointRecord): Promise; + updateCheckpoint?( + options: + | { + key: string; + status: 'success'; + value: JsonValue; + } + | { + key: string; + status: 'failed'; + reason: string; + }, + ): Promise; } // @public diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 60023bf48e..99638e48af 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -26,5 +26,4 @@ export type { TaskEventType, TaskState, TaskStatus, - CheckpointRecord, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index a7fbf4ac76..46b82ebe53 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -26,24 +26,6 @@ export type TaskSecrets = Record & { backstageToken?: string; }; -/** - * The record passed to TaskBroker for updating a checkpoint. - * Parameters to store the result of the executed checkpoint - * - * @public - */ -export type CheckpointRecord = - | { - key: string; - status: 'success'; - value: JsonValue; - } - | { - key: string; - status: 'failed'; - reason: string; - }; - /** * The state of all task's checkpoints * @@ -142,7 +124,14 @@ export interface TaskContext { cancelSignal: AbortSignal; spec: TaskSpec; secrets?: TaskSecrets; - state?: TaskState; + state?: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; createdBy?: string; done: boolean; isDryRun?: boolean; @@ -151,9 +140,33 @@ export interface TaskContext { emitLog(message: string, logMetadata?: JsonObject): Promise; - getCheckpoints?(): Promise<{ state: TaskState } | undefined>; + getTaskState?(): Promise< + | { + state: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; + } + | undefined + >; - updateCheckpoint?(options: CheckpointRecord): Promise; + updateCheckpoint?( + options: + | { + key: string; + status: 'success'; + value: JsonValue; + } + | { + key: string; + status: 'failed'; + reason: string; + }, + ): Promise; getWorkspaceName(): Promise; } From 4a460034da4ac647818957506bfe1c2147afbac5 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sat, 10 Feb 2024 14:41:10 +0100 Subject: [PATCH 023/483] wip Signed-off-by: bnechyporenko --- plugins/scaffolder-node/api-report.md | 13 ------------- plugins/scaffolder-node/src/tasks/index.ts | 1 - plugins/scaffolder-node/src/tasks/types.ts | 14 -------------- 3 files changed, 28 deletions(-) diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index c87ec8183c..7e29e16aa0 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -409,19 +409,6 @@ export type TaskSecrets = Record & { backstageToken?: string; }; -// @public -export type TaskState = { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; -}; - // @public export type TaskStatus = | 'cancelled' diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 99638e48af..930de95237 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -24,6 +24,5 @@ export type { TaskCompletionState, TaskContext, TaskEventType, - TaskState, TaskStatus, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 46b82ebe53..5a4838737c 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -26,20 +26,6 @@ export type TaskSecrets = Record & { backstageToken?: string; }; -/** - * The state of all task's checkpoints - * - * @public - */ -export type TaskState = { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; -}; - /** * The status of each step of the Task * From fb07d87c0dd0ef0446e382024aa939b952f963f4 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 11 Feb 2024 22:29:47 +0100 Subject: [PATCH 024/483] + unit test Signed-off-by: bnechyporenko --- .../tasks/NunjucksWorkflowRunner.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 3085bb8687..e6abfcb8de 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -133,6 +133,36 @@ describe('NunjucksWorkflowRunner', () => { }, }); + actionRegistry.register({ + id: 'checkpoints-action', + description: 'Mock action with checkpoints', + handler: async ctx => { + let key1 = 0; + let key2 = ''; + let i = 0; + + const incrementKey1 = async () => { + key1 += 1; + return key1; + }; + + const appendKey2 = async () => { + key2 += 'k'; + return key2; + }; + + while (i < 3) { + i += 1; + ctx.checkpoint?.('key1', incrementKey1); + } + + ctx.checkpoint?.('key2', appendKey2); + + ctx.output('key1', key1); + ctx.output('key2', key2); + }, + }); + mockedPermissionApi.authorizeConditional.mockResolvedValue([ { result: AuthorizeResult.ALLOW }, ]); @@ -538,6 +568,29 @@ describe('NunjucksWorkflowRunner', () => { ); }); + it('should deal with checkpoints', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'checkpoints-action', + input: { foo: 1 }, + }, + ], + output: { + key1: '${{steps.test.output.key1}}', + key2: '${{steps.test.output.key2}}', + }, + }); + const result = await runner.execute(task); + + expect(result.output.key1).toEqual(3); + expect(result.output.key2).toEqual('k'); + }); + it('should template the output from simple actions', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', From f94cd8bf3e65e1c06db6656bee55b810f3762380 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 11 Feb 2024 23:14:05 +0100 Subject: [PATCH 025/483] + unit test Signed-off-by: bnechyporenko --- plugins/scaffolder-backend/api-report.md | 60 ++++++------- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 32 +++---- .../tasks/NunjucksWorkflowRunner.test.ts | 89 +++++++++++-------- .../tasks/NunjucksWorkflowRunner.ts | 17 ++-- .../src/scaffolder/tasks/StorageTaskBroker.ts | 14 ++- .../src/scaffolder/tasks/types.ts | 14 ++- plugins/scaffolder-node/api-report.md | 20 ++--- plugins/scaffolder-node/src/tasks/types.ts | 14 ++- 8 files changed, 131 insertions(+), 129 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b226372d86..0bcd8ce33c 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -411,17 +411,15 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) getTaskState({ taskId }: { taskId: string }): Promise< | { - state: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; @@ -567,17 +565,15 @@ export class TaskManager implements TaskContext { // (undocumented) getTaskState?(): Promise< | { - state: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; @@ -632,17 +628,15 @@ export interface TaskStore { // (undocumented) getTaskState?({ taskId }: { taskId: string }): Promise< | { - state: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 0d6521dfd2..db890c330e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -397,14 +397,12 @@ export class DatabaseTaskStore implements TaskStore { async getTaskState({ taskId }: { taskId: string }): Promise< | { - state: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; } | undefined > { @@ -412,16 +410,14 @@ export class DatabaseTaskStore implements TaskStore { .where({ id: taskId }) .select('state'); return result.state - ? { - state: JSON.parse(result.state) as unknown as { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }, - } + ? (JSON.parse(result.state) as unknown as { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }) : undefined; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index e6abfcb8de..2b8db0d0ba 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -18,6 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; +import { JsonValue } from '@backstage/types'; import { ConfigReader } from '@backstage/config'; import { TaskContext } from './types'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -137,29 +138,19 @@ describe('NunjucksWorkflowRunner', () => { id: 'checkpoints-action', description: 'Mock action with checkpoints', handler: async ctx => { - let key1 = 0; - let key2 = ''; - let i = 0; - - const incrementKey1 = async () => { - key1 += 1; - return key1; - }; - - const appendKey2 = async () => { - key2 += 'k'; - return key2; - }; - - while (i < 3) { - i += 1; - ctx.checkpoint?.('key1', incrementKey1); - } - - ctx.checkpoint?.('key2', appendKey2); + const key1 = await ctx.checkpoint?.('key1', async () => { + return 'updated'; + }); + const key2 = await ctx.checkpoint?.('key2', async () => { + return 'updated'; + }); + const key3 = await ctx.checkpoint?.('key3', async () => { + return 'updated'; + }); ctx.output('key1', key1); ctx.output('key2', key2); + ctx.output('key3', key3); }, }); @@ -569,26 +560,52 @@ describe('NunjucksWorkflowRunner', () => { }); it('should deal with checkpoints', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'scaffolder.backstage.io/v1beta3', - parameters: {}, - steps: [ - { - id: 'test', - name: 'name', - action: 'checkpoints-action', - input: { foo: 1 }, + const task = { + ...createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'checkpoints-action', + input: { foo: 1 }, + }, + ], + output: { + key1: '${{steps.test.output.key1}}', + key2: '${{steps.test.output.key2}}', + key3: '${{steps.test.output.key3}}', }, - ], - output: { - key1: '${{steps.test.output.key1}}', - key2: '${{steps.test.output.key2}}', + }), + getTaskState: (): Promise< + | { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + } + | undefined + > => { + return Promise.resolve({ + ['v1.task.checkpoint.key1']: { + status: 'success', + value: 'initial', + }, + ['v1.task.checkpoint.key2']: { + status: 'failed', + reason: 'fatal error', + }, + }); }, - }); + }; const result = await runner.execute(task); - expect(result.output.key1).toEqual(3); - expect(result.output.key2).toEqual('k'); + expect(result.output.key1).toEqual('initial'); + expect(result.output.key2).toEqual('updated'); + expect(result.output.key3).toEqual('updated'); }); it('should template the output from simple actions', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index b5ffc73189..1dd14bc905 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -358,18 +358,21 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { try { let prevValue: U | undefined; if (prevTaskState) { - const prevState = prevTaskState.state[key]; - if (prevState.status === 'success') { + const prevState = prevTaskState[key]; + if (prevState && prevState.status === 'success') { prevValue = prevState.value as U; } } const value = prevValue ? prevValue : await fn(); - task.updateCheckpoint?.({ - key, - status: 'success', - value, - }); + + if (!prevValue) { + task.updateCheckpoint?.({ + key, + status: 'success', + value, + }); + } return value; } catch (err) { task.updateCheckpoint?.({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 453df09c77..0404f143de 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -93,14 +93,12 @@ export class TaskManager implements TaskContext { async getTaskState?(): Promise< | { - state: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; } | undefined > { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 215defd280..45c4774299 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -196,14 +196,12 @@ export interface TaskStore { getTaskState?({ taskId }: { taskId: string }): Promise< | { - state: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 7e29e16aa0..7c024a0043 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -351,17 +351,15 @@ export interface TaskContext { // (undocumented) getTaskState?(): Promise< | { - state: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 5a4838737c..18383dbac3 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -128,14 +128,12 @@ export interface TaskContext { getTaskState?(): Promise< | { - state: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; } | undefined >; From 4fb960003a5fac08d1c6bb5136c6fd575e373f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Mon, 12 Feb 2024 11:14:52 +0100 Subject: [PATCH 026/483] Parameterize LinguistCard title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- .changeset/lazy-terms-shake.md | 5 +++++ plugins/linguist/README.md | 6 ++++++ .../linguist/src/components/LinguistCard/LinguistCard.tsx | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .changeset/lazy-terms-shake.md diff --git a/.changeset/lazy-terms-shake.md b/.changeset/lazy-terms-shake.md new file mode 100644 index 0000000000..41f5292709 --- /dev/null +++ b/.changeset/lazy-terms-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist': minor +--- + +Allow to optionally pass component's title as LinguistCard parameter diff --git a/plugins/linguist/README.md b/plugins/linguist/README.md index 7ad70f1db6..72855c0894 100644 --- a/plugins/linguist/README.md +++ b/plugins/linguist/README.md @@ -79,6 +79,12 @@ To setup the Linguist Card frontend you'll need to do the following steps: ``` +3. (optionally) Set component's title - default is "Languages" + + ```tsx + + ``` + **Notes:** - The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 6a07a958c2..3b63abd406 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -55,7 +55,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export const LinguistCard = () => { +export const LinguistCard = ({ title = 'Languages' }) => { const classes = useStyles(); const theme = useTheme(); const { entity } = useEntity(); @@ -70,7 +70,7 @@ export const LinguistCard = () => { if (items && items.languageCount === 0 && items.totalBytes === 0) { return ( - + From 01fff3ff1c895ebad2d49224e7547e332bc9eea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Mon, 12 Feb 2024 11:44:29 +0100 Subject: [PATCH 027/483] generated api reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- plugins/linguist/api-report.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md index 71e2460088..ca1ce3cdd2 100644 --- a/plugins/linguist/api-report.md +++ b/plugins/linguist/api-report.md @@ -10,7 +10,11 @@ import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react'; // @public (undocumented) -export const EntityLinguistCard: () => JSX_2.Element; +export const EntityLinguistCard: ({ + title, +}: { + title?: string | undefined; +}) => JSX_2.Element; // @public (undocumented) export const isLinguistAvailable: (entity: Entity) => boolean; From e0bb6bfd5c43c79c9cf20d936e6a400e30aa695f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Mon, 12 Feb 2024 11:50:10 +0100 Subject: [PATCH 028/483] Update .changeset/lazy-terms-shake.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philipp Hugenroth Signed-off-by: Piotr Piątkiewicz --- .changeset/lazy-terms-shake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lazy-terms-shake.md b/.changeset/lazy-terms-shake.md index 41f5292709..62fd2d848e 100644 --- a/.changeset/lazy-terms-shake.md +++ b/.changeset/lazy-terms-shake.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-linguist': minor +'@backstage/plugin-linguist': patch --- Allow to optionally pass component's title as LinguistCard parameter From d294557f5ec2727e3485fb2f4936e1bd312bade8 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:27:53 +0000 Subject: [PATCH 029/483] Better example annotation value Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index b5bcdaec73..4cabc647f0 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -11,5 +11,5 @@ Defaults to the current, AZD default behaviour (`README.md` in the root of the g Example: ```yaml -dev.azure.com/readme-path: /my-path/CHANGELOG.md +dev.azure.com/readme-path: /my-path/README.md ``` From ad528cc4957f45eab617c7f551efe57a83019ff9 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:28:39 +0000 Subject: [PATCH 030/483] Better instructions for implementation Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- plugins/azure-devops/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index c1191b1566..6521f0e237 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -63,7 +63,7 @@ spec: #### Mono repos -If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity: +If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity, like this: ```yaml dev.azure.com/project-repo: / From b0002dfca3ba023ab0753e9fc3804ca93826e4d1 Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:22:53 +0000 Subject: [PATCH 031/483] Replace acronym AZD with the full name Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index 4cabc647f0..44963ec773 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -6,7 +6,7 @@ Ability to fetch the README file from a different AZD path. -Defaults to the current, AZD default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` +Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` Example: From fed13da396381f3678becf864ee5d6f8ed781c3b Mon Sep 17 00:00:00 2001 From: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:23:05 +0000 Subject: [PATCH 032/483] Replace acronym AZD with the full name Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: David Roberts <61826520+DavidRobertsOrbis@users.noreply.github.com> --- .changeset/itchy-news-drive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-news-drive.md b/.changeset/itchy-news-drive.md index 44963ec773..97cca5b155 100644 --- a/.changeset/itchy-news-drive.md +++ b/.changeset/itchy-news-drive.md @@ -4,7 +4,7 @@ '@backstage/plugin-azure-devops': minor --- -Ability to fetch the README file from a different AZD path. +Ability to fetch the README file from a different Azure DevOps path. Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` From fe8e16df4a20c93c86722010585c7663ef23519d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Mon, 12 Feb 2024 15:44:05 +0100 Subject: [PATCH 033/483] Get linguist component title from translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- .changeset/lazy-terms-shake.md | 2 +- plugins/linguist/README.md | 6 ----- plugins/linguist/api-report.md | 6 +---- .../components/LinguistCard/LinguistCard.tsx | 7 +++-- plugins/linguist/src/translation.ts | 26 +++++++++++++++++++ 5 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 plugins/linguist/src/translation.ts diff --git a/.changeset/lazy-terms-shake.md b/.changeset/lazy-terms-shake.md index 62fd2d848e..03c0fee21f 100644 --- a/.changeset/lazy-terms-shake.md +++ b/.changeset/lazy-terms-shake.md @@ -2,4 +2,4 @@ '@backstage/plugin-linguist': patch --- -Allow to optionally pass component's title as LinguistCard parameter +Get component's title from translation file diff --git a/plugins/linguist/README.md b/plugins/linguist/README.md index 72855c0894..7ad70f1db6 100644 --- a/plugins/linguist/README.md +++ b/plugins/linguist/README.md @@ -79,12 +79,6 @@ To setup the Linguist Card frontend you'll need to do the following steps: ``` -3. (optionally) Set component's title - default is "Languages" - - ```tsx - - ``` - **Notes:** - The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md index ca1ce3cdd2..71e2460088 100644 --- a/plugins/linguist/api-report.md +++ b/plugins/linguist/api-report.md @@ -10,11 +10,7 @@ import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react'; // @public (undocumented) -export const EntityLinguistCard: ({ - title, -}: { - title?: string | undefined; -}) => JSX_2.Element; +export const EntityLinguistCard: () => JSX_2.Element; // @public (undocumented) export const isLinguistAvailable: (entity: Entity) => boolean; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 3b63abd406..95aa5e0d89 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -27,6 +27,8 @@ import React from 'react'; import slugify from 'slugify'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useLanguages } from '../../hooks'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { linguistTranslationRef } from '../../translation'; const useStyles = makeStyles(theme => ({ infoCard: { @@ -55,7 +57,8 @@ const useStyles = makeStyles(theme => ({ }, })); -export const LinguistCard = ({ title = 'Languages' }) => { +export const LinguistCard = () => { + const { t } = useTranslationRef(linguistTranslationRef); const classes = useStyles(); const theme = useTheme(); const { entity } = useEntity(); @@ -70,7 +73,7 @@ export const LinguistCard = ({ title = 'Languages' }) => { if (items && items.languageCount === 0 && items.totalBytes === 0) { return ( - + diff --git a/plugins/linguist/src/translation.ts b/plugins/linguist/src/translation.ts new file mode 100644 index 0000000000..78a34f2f7e --- /dev/null +++ b/plugins/linguist/src/translation.ts @@ -0,0 +1,26 @@ +/* + * 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const linguistTranslationRef = createTranslationRef({ + id: 'linguist', + messages: { + component: { + title: 'Languages', + }, + }, +}); From 5bc5d5cf30fd328d96b42d772b9330777f420911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Mon, 12 Feb 2024 16:52:55 +0100 Subject: [PATCH 034/483] Export translation in linguist package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- plugins/linguist/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 4d308b2f73..dbc5da2d31 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -10,7 +10,8 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./src/translation": "./src/translation.ts" }, "typesVersions": { "*": { From d8294546d8edca6f4c87d2027510d4bfc71ccdb6 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 15:51:32 +0000 Subject: [PATCH 035/483] add a test for subfolder as well as filename Signed-off-by: David Roberts --- .../src/service/router.test.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 9e8f968826..8793860708 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -482,7 +482,7 @@ describe('createRouter', () => { }); describe('GET /readme/:projectName/:repoName', () => { - it('fetches default readme file', async () => { + it('fetches default default readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README.md`; @@ -507,7 +507,7 @@ describe('createRouter', () => { }); }); - describe('GET /readme/:projectName/:repoName with readme path', () => { + describe('GET /readme/:projectName/:repoName with readme filename', () => { it('fetches specified readme file', async () => { const content = getReadmeMock(); const url = `https://host.com/myOrg/myProject/_git/myRepo?path=README_NOT_DEFAULT.md`; @@ -534,6 +534,34 @@ describe('createRouter', () => { }); }); }); + + describe('GET /readme/:projectName/:repoName with readme path', () => { + it('fetches specified readme file from subfolder', async () => { + const content = getReadmeMock(); + const url = `https://host.com/myOrg/myProject/_git/myRepo?path=/my-path/README.md`; + + azureDevOpsApi.getReadme.mockResolvedValueOnce({ + content, + url, + }); + + const response = await request(app).get( + '/readme/myProject/myRepo?path=/my-path/README.md', + ); + expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( + 'host.com', + 'myOrg', + 'myProject', + 'myRepo', + '/my-path/README.md', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + content, + url, + }); + }); + }); }); function getReadmeMock() { From 5bda681a1cc8ee5dd24753c1a5450af1073106b7 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:13:24 +0000 Subject: [PATCH 036/483] add tests for readme extraction Signed-off-by: David Roberts --- .../getAnnotationValuesFromEntity.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 9325a79039..e196e9dfc7 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -52,6 +52,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: undefined, org: undefined, }); @@ -149,6 +150,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, host: undefined, org: undefined, }); @@ -220,6 +222,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: 'hostName', org: 'organizationName', }); @@ -246,6 +249,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, host: 'hostName', org: 'organizationName', }); @@ -344,6 +348,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: undefined, + readmePath: undefined, host: 'company.com/tfs', org: 'organizationName', }); @@ -417,6 +422,7 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: 'repoName', definition: 'buildDefinitionName', + readmePath: undefined, host: undefined, org: undefined, }); @@ -443,6 +449,87 @@ describe('getAnnotationValuesFromEntity', () => { project: 'projectName', repo: undefined, definition: 'buildDefinitionName', + readmePath: undefined, + host: undefined, + org: undefined, + }); + }); + }); + + describe('definition, project and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + readmePath: 'readme/path.md', + host: undefined, + org: undefined, + }); + }); + }); + + describe('definition, projectRepo and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: 'buildDefinitionName', + readmePath: 'readme/path.md', + host: undefined, + org: undefined, + }); + }); + }); + + describe('projectRepo and readme', () => { + it('returns with the readme path', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/readme-path': 'readme/path.md', + }, + }, + }; + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + readmePath: 'readme/path.md', host: undefined, org: undefined, }); From a29f6833bb82476641d54af959fe599f92531b1f Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:31:36 +0000 Subject: [PATCH 037/483] explicit rejection of bad parameter types Signed-off-by: David Roberts --- .../azure-devops-backend/src/service/router.test.ts | 10 ++++++++++ plugins/azure-devops-backend/src/service/router.ts | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 8793860708..7bf558612f 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -562,6 +562,16 @@ describe('createRouter', () => { }); }); }); + + describe('GET /readme/:projectName/:repoName with a bad readme path (multiple values)', () => { + it('throws InputError', async () => { + const response = await request(app).get( + '/readme/myProject/myRepo?path=1&path=2', + ); + expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); + }); + }); }); function getReadmeMock() { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index a00b0ce910..230ff08c8b 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { Logger } from 'winston'; import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider'; import Router from 'express-promise-router'; import { errorHandler, UrlReader } from '@backstage/backend-common'; +import { InputError } from '@backstage/errors'; import express from 'express'; const DEFAULT_TOP = 10; @@ -216,7 +217,17 @@ export async function createRouter( req.query.host?.toString() ?? config.getString('azureDevOps.host'); const org = req.query.org?.toString() ?? config.getString('azureDevOps.organization'); - const path = req.query.path?.toString() ?? 'README.md'; + let path = req.query.path; + + if (path === undefined) { + // if the annotation is missing, default to the previous behaviour (look for README.md in the root of the repo) + path = 'README.md'; + } + + if (typeof path !== 'string') { + throw new InputError('Invalid path param'); + } + const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, From 184c8877c99a4d2338e82cd1be8bed72eb9c4784 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 16:49:47 +0000 Subject: [PATCH 038/483] throw if we are passed the empty string - we know this won't work Signed-off-by: David Roberts --- plugins/azure-devops-backend/src/service/router.test.ts | 8 ++++++++ plugins/azure-devops-backend/src/service/router.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 7bf558612f..c12b13a5aa 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -572,6 +572,14 @@ describe('createRouter', () => { expect(response.status).toEqual(400); }); }); + + describe('GET /readme/:projectName/:repoName with a bad readme path (empty string)', () => { + it('throws InputError', async () => { + const response = await request(app).get('/readme/myProject/myRepo?path='); + expect(azureDevOpsApi.getReadme).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); + }); + }); }); function getReadmeMock() { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 230ff08c8b..bc13a4eee2 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -228,6 +228,10 @@ export async function createRouter( throw new InputError('Invalid path param'); } + if (path === '') { + throw new InputError('If present, the path param should not be empty'); + } + const { projectName, repoName } = req.params; const readme = await azureDevOpsApi.getReadme( host, From 1f60f53d247094475a3f73df25e7da5386109643 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Feb 2024 17:32:08 +0000 Subject: [PATCH 039/483] add explicit dependency on the errors package Signed-off-by: David Roberts --- plugins/azure-devops-backend/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 41b1839ce4..22889db6a1 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -32,6 +32,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-azure-devops-common": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 0e90b16d15..2415b3a4fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5020,6 +5020,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-azure-devops-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" From 8d3734b42af16f1322d37ce26da7265eebc52f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Tue, 13 Feb 2024 08:37:06 +0100 Subject: [PATCH 040/483] fix translation import, added one more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- .changeset/lazy-terms-shake.md | 2 +- plugins/linguist/api-report-alpha.md | 10 ++++++++++ plugins/linguist/package.json | 3 +-- plugins/linguist/src/alpha.ts | 1 + .../src/components/LinguistCard/LinguistCard.tsx | 4 +--- plugins/linguist/src/translation.ts | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.changeset/lazy-terms-shake.md b/.changeset/lazy-terms-shake.md index 03c0fee21f..efe89f1c12 100644 --- a/.changeset/lazy-terms-shake.md +++ b/.changeset/lazy-terms-shake.md @@ -2,4 +2,4 @@ '@backstage/plugin-linguist': patch --- -Get component's title from translation file +Get component's title from translation file. See: https://backstage.io/docs/plugins/internationalization#for-an-application-developer-overwrite-plugin-messages diff --git a/plugins/linguist/api-report-alpha.md b/plugins/linguist/api-report-alpha.md index 418439d331..4a250535b0 100644 --- a/plugins/linguist/api-report-alpha.md +++ b/plugins/linguist/api-report-alpha.md @@ -4,10 +4,20 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) const _default: BackstagePlugin<{}, {}>; export default _default; +// @alpha (undocumented) +export const linguistTranslationRef: TranslationRef< + 'linguist', + { + readonly 'component.title': 'Languages'; + readonly 'component.noData': 'There is currently no language data for this entity.'; + } +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index dbc5da2d31..4d308b2f73 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -10,8 +10,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", - "./package.json": "./package.json", - "./src/translation": "./src/translation.ts" + "./package.json": "./package.json" }, "typesVersions": { "*": { diff --git a/plugins/linguist/src/alpha.ts b/plugins/linguist/src/alpha.ts index e80f131817..287775ade0 100644 --- a/plugins/linguist/src/alpha.ts +++ b/plugins/linguist/src/alpha.ts @@ -16,3 +16,4 @@ export * from './alpha/index'; export { default } from './alpha/index'; +export * from './translation'; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 95aa5e0d89..3a07dd35e2 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -76,9 +76,7 @@ export const LinguistCard = () => { - - There is currently no language data for this entity. - + {t('component.noData')} diff --git a/plugins/linguist/src/translation.ts b/plugins/linguist/src/translation.ts index 78a34f2f7e..f971b70490 100644 --- a/plugins/linguist/src/translation.ts +++ b/plugins/linguist/src/translation.ts @@ -21,6 +21,7 @@ export const linguistTranslationRef = createTranslationRef({ messages: { component: { title: 'Languages', + noData: 'There is currently no language data for this entity.', }, }, }); From 9c7e28a36e232456955e59123cd5d399d33e0457 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 13 Feb 2024 10:31:33 +0100 Subject: [PATCH 041/483] + unit test Signed-off-by: bnechyporenko --- package.json | 1 - plugins/scaffolder-backend/api-report.md | 4 +-- .../tasks/DatabaseTaskStore.test.ts | 27 +++++++++++++++++++ .../src/scaffolder/tasks/DatabaseTaskStore.ts | 2 +- .../src/scaffolder/tasks/StorageTaskBroker.ts | 2 +- .../src/scaffolder/tasks/types.ts | 2 +- yarn.lock | 1 - 7 files changed, 32 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d3188d483a..646b9e43c8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@backstage/repo-tools": "workspace:*", "@changesets/cli": "^2.14.0", "@octokit/rest": "^19.0.3", - "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^14.1.3", "@spotify/prettier-config": "^14.0.0", "@techdocs/cli": "workspace:*", diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 0bcd8ce33c..151c8568d5 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -445,7 +445,7 @@ export class DatabaseTaskStore implements TaskStore { ids: string[]; }>; // (undocumented) - saveCheckpoint(options: { + saveTaskState(options: { taskId: string; state?: | { @@ -661,7 +661,7 @@ export interface TaskStore { ids: string[]; }>; // (undocumented) - saveCheckpoint?(options: { + saveTaskState?(options: { taskId: string; state?: { [key: string]: diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index 4c0eb94c82..deb72fb7d2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -188,4 +188,31 @@ describe('DatabaseTaskStore', () => { await store.shutdownTask({ taskId }); }).rejects.toThrow(ConflictError); }); + + it('should store checkpoints and retrieve task state', async () => { + const { store } = await createStore(); + const { taskId } = await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'me', + }); + + await store.saveTaskState({ + taskId, + state: { + 'repo.create': { + status: 'success', + value: { repoUrl: 'https://github.com/backstage/backstage.git' }, + }, + }, + }); + + const state = await store.getTaskState({ taskId }); + + expect(state).toStrictEqual({ + 'repo.create': { + status: 'success', + value: { repoUrl: 'https://github.com/backstage/backstage.git' }, + }, + }); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index db890c330e..263fe3d435 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -421,7 +421,7 @@ export class DatabaseTaskStore implements TaskStore { : undefined; } - async saveCheckpoint(options: { + async saveTaskState(options: { taskId: string; state?: | { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 0404f143de..cc958b35b3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -124,7 +124,7 @@ export class TaskManager implements TaskContext { } else { this.task.state = { [key]: value }; } - await this.storage.saveCheckpoint?.({ + await this.storage.saveTaskState?.({ taskId: this.task.taskId, state: this.task.state, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 45c4774299..d0bb48f4f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -206,7 +206,7 @@ export interface TaskStore { | undefined >; - saveCheckpoint?(options: { + saveTaskState?(options: { taskId: string; state?: { [key: string]: diff --git a/yarn.lock b/yarn.lock index 6379617840..0d58ba4587 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41307,7 +41307,6 @@ __metadata: "@changesets/cli": ^2.14.0 "@manypkg/get-packages": ^1.1.3 "@octokit/rest": ^19.0.3 - "@playwright/test": ^1.32.3 "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" From 59d1a43b468e914d263b53badfb976048bb3162d Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 09:36:32 +0000 Subject: [PATCH 042/483] fix: failing test relating to changes made to github-pull-requests-board Signed-off-by: Josh Uvi --- .../EntityTeamPullRequestsCard.test.tsx | 62 ++++- .../EntityTeamPullRequestsContent.test.tsx | 232 ++++++++++++------ 2 files changed, 214 insertions(+), 80 deletions(-) diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx index 219e587971..a8ef9a9e2c 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { EntityTeamPullRequestsCard } from '../EntityTeamPullRequestsCard'; -import { PullRequestsColumn } from '../../utils/types'; +import { PullRequestsColumn, Status } from '../../utils/types'; import { render } from '@testing-library/react'; import { fireEvent } from '@testing-library/react'; @@ -39,6 +39,7 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { repoName: string, isDraft: boolean, isArchived: boolean, + status: Status, ) => { return { id: 'id', @@ -62,6 +63,9 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { labels: { nodes: [], }, + commits: { + nodes: status, + }, isDraft: isDraft, author: { login: authorLogin, @@ -83,6 +87,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', false, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-non-draft-is-archive', @@ -90,6 +101,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', false, true, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-is-draft-non-archive', @@ -97,6 +115,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', true, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'non-team-is-draft-is-archive', @@ -104,6 +129,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'team-repo', true, true, + { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, ), buildPullRequest( 'is-team-non-draft-non-archive', @@ -111,6 +143,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', false, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-non-draft-is-archive', @@ -118,6 +157,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', false, true, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-is-draft-non-archive', @@ -125,6 +171,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', true, false, + { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, ), buildPullRequest( 'is-team-is-draft-is-archive', @@ -132,6 +185,13 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { 'non-team-repo', true, true, + { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, ), ], }, diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx index 35c323a4f1..dc02030f47 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { EntityTeamPullRequestsContent } from '../EntityTeamPullRequestsContent'; -import { PullRequestsColumn } from '../../utils/types'; +import { PullRequestsColumn, Status } from '../../utils/types'; import { render } from '@testing-library/react'; import { fireEvent } from '@testing-library/react'; @@ -33,13 +33,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => { }); jest.mock('../../hooks/usePullRequestsByTeam', () => { - const buildPullRequest = ( - prTitle: string, - authorLogin: string, - repoName: string, - isDraft: boolean, - isArchived: boolean, - ) => { + const buildPullRequest = ({ + prTitle, + authorLogin, + repoName, + isDraft, + isArchived, + status, + }: { + prTitle: string; + authorLogin: string; + repoName: string; + isDraft: boolean; + isArchived: boolean; + status: Status; + }) => { return { id: 'id', title: prTitle, @@ -62,6 +70,9 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { labels: { nodes: [], }, + commits: { + nodes: status, + }, isDraft: isDraft, author: { login: authorLogin, @@ -77,62 +88,118 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { { title: 'column', content: [ - buildPullRequest( - 'non-team-non-draft-non-archive', - 'non-team-member', - 'team-repo', - false, - false, - ), - buildPullRequest( - 'non-team-non-draft-is-archive', - 'non-team-member', - 'team-repo', - false, - true, - ), - buildPullRequest( - 'non-team-is-draft-non-archive', - 'non-team-member', - 'team-repo', - true, - false, - ), - buildPullRequest( - 'non-team-is-draft-is-archive', - 'non-team-member', - 'team-repo', - true, - true, - ), - buildPullRequest( - 'is-team-non-draft-non-archive', - 'team-member', - 'non-team-repo', - false, - false, - ), - buildPullRequest( - 'is-team-non-draft-is-archive', - 'team-member', - 'non-team-repo', - false, - true, - ), - buildPullRequest( - 'is-team-is-draft-non-archive', - 'team-member', - 'non-team-repo', - true, - false, - ), - buildPullRequest( - 'is-team-is-draft-is-archive', - 'team-member', - 'non-team-repo', - true, - true, - ), + buildPullRequest({ + prTitle: 'non-team-non-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-non-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: false, + status: { + commit: { + statusCheckRollup: { + state: 'FAILURE', + }, + }, + }, + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: true, + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, + }), ], }, ]; @@ -152,7 +219,7 @@ describe('EntityTeamPullRequestsContent', () => { describe('non-team PRs', () => { describe('non-draft PRs', () => { it('should show non-team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, queryAllByTitle } = await render( + const { getByText, getAllByText, queryAllByTitle } = render( , ); expect(getByText('non-team-non-draft-non-archive')).toBeInTheDocument(); @@ -162,8 +229,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show non-team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const archiveToggle = getByTitle('Show archived repos'); fireEvent.click(archiveToggle); expect(getByText('non-team-non-draft-is-archive')).toBeInTheDocument(); @@ -175,8 +243,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('draft PRs', () => { it('should show draft non-team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const draftToggle = getByTitle('Show draft PRs'); fireEvent.click(draftToggle); expect(getByText('non-team-is-draft-non-archive')).toBeInTheDocument(); @@ -186,8 +255,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show draft non-team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const draftToggle = getByTitle('Show draft PRs'); fireEvent.click(draftToggle); const archiveToggle = getByTitle('Show archived repos'); @@ -203,8 +273,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('team PRs', () => { describe('non-draft PRs', () => { it('should show team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); expect(getByText('is-team-non-draft-non-archive')).toBeInTheDocument(); @@ -214,8 +285,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const archiveToggle = getByTitle('Show archived repos'); @@ -229,8 +301,9 @@ describe('EntityTeamPullRequestsContent', () => { describe('draft PRs', () => { it('should show draft team PRs for un-archived repos when archived option is not checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const draftToggle = getByTitle('Show draft PRs'); @@ -242,8 +315,9 @@ describe('EntityTeamPullRequestsContent', () => { }); it('should show draft team PRs for archived repos when archived option is checked', async () => { - const { getByText, getAllByText, getByTitle, queryAllByTitle } = - await render(); + const { getByText, getAllByText, getByTitle, queryAllByTitle } = render( + , + ); const teamToggle = getByTitle('Show PRs from your team'); fireEvent.click(teamToggle); const draftToggle = getByTitle('Show draft PRs'); From d1d602e6d971529160b45479396a561d22714590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Tue, 13 Feb 2024 10:39:44 +0100 Subject: [PATCH 043/483] updated translations keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- plugins/linguist/api-report-alpha.md | 4 ++-- plugins/linguist/src/components/LinguistCard/LinguistCard.tsx | 4 ++-- plugins/linguist/src/translation.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/linguist/api-report-alpha.md b/plugins/linguist/api-report-alpha.md index 4a250535b0..07d2244e30 100644 --- a/plugins/linguist/api-report-alpha.md +++ b/plugins/linguist/api-report-alpha.md @@ -14,8 +14,8 @@ export default _default; export const linguistTranslationRef: TranslationRef< 'linguist', { - readonly 'component.title': 'Languages'; - readonly 'component.noData': 'There is currently no language data for this entity.'; + readonly 'entityCard.title': 'Languages'; + readonly 'entityCard.noData': 'There is currently no language data for this entity.'; } >; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 3a07dd35e2..19225f1471 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -73,10 +73,10 @@ export const LinguistCard = () => { if (items && items.languageCount === 0 && items.totalBytes === 0) { return ( - + - {t('component.noData')} + {t('entityCard.noData')} diff --git a/plugins/linguist/src/translation.ts b/plugins/linguist/src/translation.ts index f971b70490..7055d3dfe8 100644 --- a/plugins/linguist/src/translation.ts +++ b/plugins/linguist/src/translation.ts @@ -19,7 +19,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; export const linguistTranslationRef = createTranslationRef({ id: 'linguist', messages: { - component: { + entityCard: { title: 'Languages', noData: 'There is currently no language data for this entity.', }, From d8eea9b165220c609b4aae726cfd2b06a49259d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Pi=C4=85tkiewicz?= Date: Tue, 13 Feb 2024 12:22:31 +0100 Subject: [PATCH 044/483] added missing translation reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piotr Piątkiewicz --- plugins/linguist/src/components/LinguistCard/LinguistCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 19225f1471..213ca316ca 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -89,7 +89,7 @@ export const LinguistCard = () => { const processedDate = items?.processedDate; return breakdown && processedDate ? ( - + {breakdown.map((language, index: number) => { barWidth = barWidth + language.percentage; From 30fadd61daf864217304fe0a203f15af4d78a3fd Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 13 Feb 2024 13:25:12 +0100 Subject: [PATCH 045/483] refactor: removed he and added a util to decode html Signed-off-by: Tommy Le --- plugins/stack-overflow/package.json | 2 -- .../StackOverflowSearchResultListItem.tsx | 15 ++++++------- plugins/stack-overflow/src/util.ts | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 plugins/stack-overflow/src/util.ts diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 73498e2a53..c1a4f8f40b 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -54,7 +54,6 @@ "@testing-library/jest-dom": "^6.0.0", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "cross-fetch": "^4.0.0", - "he": "^1.2.0", "lodash": "^4.17.21", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -70,7 +69,6 @@ "@testing-library/dom": "^9.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", - "@types/he": "^1.2.3", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx index 9e98d6612f..70e182b86d 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx @@ -27,7 +27,7 @@ import { import { useAnalytics } from '@backstage/core-plugin-api'; import type { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -import { decode } from 'he'; +import { decodeHtml } from '../../util'; /** * Props for {@link StackOverflowSearchResultListItem} @@ -45,13 +45,10 @@ export const StackOverflowSearchResultListItem = ( props: StackOverflowSearchResultListItemProps, ) => { const { result, highlight } = props; + const analytics = useAnalytics(); const handleClick = () => { - if (!result) { - return; - } - analytics.captureEvent('discover', result.title, { attributes: { to: result.location }, value: props.rank, @@ -73,12 +70,12 @@ export const StackOverflowSearchResultListItem = ( {highlight?.fields?.title ? ( ) : ( - decode(result.title) + decodeHtml(result.title) )} } @@ -87,13 +84,13 @@ export const StackOverflowSearchResultListItem = ( <> Author:{' '} ) : ( - `Author: ${decode(result.text)}` + `Author: ${decodeHtml(result.text)}` ) } /> diff --git a/plugins/stack-overflow/src/util.ts b/plugins/stack-overflow/src/util.ts new file mode 100644 index 0000000000..a40ae905ea --- /dev/null +++ b/plugins/stack-overflow/src/util.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export function decodeHtml(input: string) { + const textContainer = document.createElement('textarea'); + textContainer.innerHTML = input; + return textContainer.value; +} From 6faf825b662a6e9a9a64d22d9fe55fec81bbe1f8 Mon Sep 17 00:00:00 2001 From: Tommy Le Date: Tue, 13 Feb 2024 13:50:35 +0100 Subject: [PATCH 046/483] chore: update api-report Signed-off-by: Tommy Le --- plugins/stack-overflow/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stack-overflow/api-report.md b/plugins/stack-overflow/api-report.md index 293c2ac226..7593db530f 100644 --- a/plugins/stack-overflow/api-report.md +++ b/plugins/stack-overflow/api-report.md @@ -10,7 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CardExtensionProps } from '@backstage/plugin-home-react'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; -import { ResultHighlight } from '@backstage/plugin-search-common'; +import type { ResultHighlight } from '@backstage/plugin-search-common'; import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; // @public From 6899c2dcf5f5b727484a7f310c9dd8dfa802fd2c Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 12:57:29 +0000 Subject: [PATCH 047/483] feat: adds last commit status to PR cardheader component and changeset Signed-off-by: Josh Uvi --- .../src/components/Card/CardHeader.test.tsx | 13 ++ .../src/components/Card/CardHeader.tsx | 4 +- .../EntityTeamPullRequestsCard.test.tsx | 151 +++++++++--------- .../EntityTeamPullRequestsCard.tsx | 2 + .../EntityTeamPullRequestsContent.tsx | 3 - .../src/utils/types.tsx | 2 +- 6 files changed, 97 insertions(+), 78 deletions(-) diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx index 3171bff10f..0ca1bfd43f 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.test.tsx @@ -37,6 +37,13 @@ const props = { name: 'documentation', }, ], + status: { + commit: { + statusCheckRollup: { + state: 'SUCCESS', + }, + }, + }, }; describe('', () => { @@ -54,4 +61,10 @@ describe('', () => { await renderInTestApp(); expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); }); + + it('finds commit status in PR Card Header', async () => { + await renderInTestApp(); + expect(screen.getByText('Commit Status:')).toBeInTheDocument(); + expect(props.status.commit.statusCheckRollup.state).toBeTruthy(); + }); }); diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx index 98d53f781c..9a47938139 100644 --- a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx +++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx @@ -48,7 +48,7 @@ const CardHeader: FunctionComponent = (props: Props) => { isDraft, repositoryIsArchived, labels, - status: commitStatus, + status, } = props; return ( @@ -91,7 +91,7 @@ const CardHeader: FunctionComponent = (props: Props) => { Commit Status:{' '} - {commitStatus.commit.statusCheckRollup.state} + {status.commit.statusCheckRollup.state} {labels && ( diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx index a8ef9a9e2c..a4e0240cbd 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.test.tsx @@ -33,14 +33,21 @@ jest.mock('../../hooks/useUserRepositoriesAndTeam', () => { }); jest.mock('../../hooks/usePullRequestsByTeam', () => { - const buildPullRequest = ( - prTitle: string, - authorLogin: string, - repoName: string, - isDraft: boolean, - isArchived: boolean, - status: Status, - ) => { + const buildPullRequest = ({ + prTitle, + authorLogin, + repoName, + isDraft, + isArchived, + status, + }: { + prTitle: string; + authorLogin: string; + repoName: string; + isDraft: boolean; + isArchived: boolean; + status: Status; + }) => { return { id: 'id', title: prTitle, @@ -81,118 +88,118 @@ jest.mock('../../hooks/usePullRequestsByTeam', () => { { title: 'column', content: [ - buildPullRequest( - 'non-team-non-draft-non-archive', - 'non-team-member', - 'team-repo', - false, - false, - { + buildPullRequest({ + prTitle: 'non-team-non-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-non-draft-is-archive', - 'non-team-member', - 'team-repo', - false, - true, - { + }), + buildPullRequest({ + prTitle: 'non-team-non-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: false, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-is-draft-non-archive', - 'non-team-member', - 'team-repo', - true, - false, - { + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-non-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'non-team-is-draft-is-archive', - 'non-team-member', - 'team-repo', - true, - true, - { + }), + buildPullRequest({ + prTitle: 'non-team-is-draft-is-archive', + authorLogin: 'non-team-member', + repoName: 'team-repo', + isDraft: true, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'SUCCESS', }, }, }, - ), - buildPullRequest( - 'is-team-non-draft-non-archive', - 'team-member', - 'non-team-repo', - false, - false, - { + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-non-draft-is-archive', - 'team-member', - 'non-team-repo', - false, - true, - { + }), + buildPullRequest({ + prTitle: 'is-team-non-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: false, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-is-draft-non-archive', - 'team-member', - 'non-team-repo', - true, - false, - { + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-non-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: false, + status: { commit: { statusCheckRollup: { state: 'FAILURE', }, }, }, - ), - buildPullRequest( - 'is-team-is-draft-is-archive', - 'team-member', - 'non-team-repo', - true, - true, - { + }), + buildPullRequest({ + prTitle: 'is-team-is-draft-is-archive', + authorLogin: 'team-member', + repoName: 'non-team-repo', + isDraft: true, + isArchived: true, + status: { commit: { statusCheckRollup: { state: 'SUCCESS', }, }, }, - ), + }), ], }, ]; diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx index 25ef32362b..9285d6b032 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx @@ -114,6 +114,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => { repository, isDraft, labels, + commits, }, index, ) => @@ -137,6 +138,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => { repositoryIsArchived={repository.isArchived} isDraft={isDraft} labels={labels.nodes} + status={commits.nodes} /> ), )} diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx index 6109ddb63c..668995a306 100644 --- a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx +++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx @@ -87,9 +87,6 @@ const EntityTeamPullRequestsContent = ( return ; } - // eslint-disable-next-line no-console - console.log('pull - ', pullRequests); - return ( {pullRequests.length ? ( diff --git a/plugins/github-pull-requests-board/src/utils/types.tsx b/plugins/github-pull-requests-board/src/utils/types.tsx index cdabcf0713..88b05219d6 100644 --- a/plugins/github-pull-requests-board/src/utils/types.tsx +++ b/plugins/github-pull-requests-board/src/utils/types.tsx @@ -84,7 +84,7 @@ export type Label = { export type Status = { commit: { statusCheckRollup: { - state: 'SUCCESS' | 'FAILURE' | 'ERROR' | 'EXPECTED' | 'PENDING'; + state: string; }; }; }; From 3c2d7c0e720a9c412345bb5868857811bdf77d97 Mon Sep 17 00:00:00 2001 From: Josh Uvi Date: Tue, 13 Feb 2024 13:22:36 +0000 Subject: [PATCH 048/483] adds changeset Signed-off-by: Josh Uvi --- .changeset/chilled-dolphins-tap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-dolphins-tap.md diff --git a/.changeset/chilled-dolphins-tap.md b/.changeset/chilled-dolphins-tap.md new file mode 100644 index 0000000000..92549aaef0 --- /dev/null +++ b/.changeset/chilled-dolphins-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-pull-requests-board': patch +--- + +The cardheader component in the github-pull-requests-board plugin now requires that a `status` is passed to the component. From c04d4bcc9dde841803ac83ada3ce98c3b5a9efe8 Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 15:30:53 -0700 Subject: [PATCH 049/483] Update descriptor-format.md Adding table for profile fields. Signed-off-by: Tyler Wray --- .../software-catalog/descriptor-format.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index cc2b3d1c7b..9adfe47841 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -955,6 +955,14 @@ some form, that the group may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the group, and that a browser could fetch and render on a group page or similar. +The fields of a profile are: + +| Field | Type | Description | +| ------------------------ | ------ | -------------------------------------------------------------- | +| `displayName` (optional) | String | A human-readable name for the group. | +| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | + ### `spec.parent` [optional] The immediate parent group in the hierarchy, if any. Not all groups must have a @@ -1040,6 +1048,14 @@ of some form, that the user may wish to be used for contacting them. The picture is expected to be a URL pointing to an image that's representative of the user, and that a browser could fetch and render on a profile page or similar. +The fields of a profile are: + +| Field | Type | Description | +| ------------------------ | ------ | -------------------------------------------------------------- | +| `displayName` (optional) | String | A human-readable name for the group. | +| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | + ### `spec.memberOf` [required] The list of groups that the user is a direct member of (i.e., no transitive From 9bb9a4806c2a0e39db195e1ff20fb683dbb00d03 Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:30 -0700 Subject: [PATCH 050/483] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 9adfe47841..4b54f648f8 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1054,7 +1054,7 @@ The fields of a profile are: | ------------------------ | ------ | -------------------------------------------------------------- | | `displayName` (optional) | String | A human-readable name for the group. | | `email` (optional) | String | An email the group may wish to be used for contacting them. | -| `picture` (optional) | String | A URL pointing to an image that's representative of the group. | +| `picture` (optional) | String | A URL pointing to an image that's representative of the user. | ### `spec.memberOf` [required] From f095b4b892971011676930aebfeaa0fb1d7e318b Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:33 -0700 Subject: [PATCH 051/483] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 4b54f648f8..b0d74f8ab2 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1053,7 +1053,7 @@ The fields of a profile are: | Field | Type | Description | | ------------------------ | ------ | -------------------------------------------------------------- | | `displayName` (optional) | String | A human-readable name for the group. | -| `email` (optional) | String | An email the group may wish to be used for contacting them. | +| `email` (optional) | String | An email the user may wish to be used for contacting them. | | `picture` (optional) | String | A URL pointing to an image that's representative of the user. | ### `spec.memberOf` [required] From f321ad476632eef5a5b9271c3a55d174f20fcf3d Mon Sep 17 00:00:00 2001 From: Tyler Wray Date: Tue, 13 Feb 2024 19:19:41 -0700 Subject: [PATCH 052/483] Update docs/features/software-catalog/descriptor-format.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Tyler Wray --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index b0d74f8ab2..41bd81019b 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1052,7 +1052,7 @@ The fields of a profile are: | Field | Type | Description | | ------------------------ | ------ | -------------------------------------------------------------- | -| `displayName` (optional) | String | A human-readable name for the group. | +| `displayName` (optional) | String | A human-readable name for the user. | | `email` (optional) | String | An email the user may wish to be used for contacting them. | | `picture` (optional) | String | A URL pointing to an image that's representative of the user. | From 38af71a3a08ac45720544b68461242a653751b19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 00:30:26 +0000 Subject: [PATCH 053/483] fix(deps): update dependency google-auth-library to v9 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-8f23b96.md | 7 ++ .../package.json | 2 +- .../package.json | 2 +- plugins/auth-backend/package.json | 2 +- yarn.lock | 74 +------------------ 5 files changed, 13 insertions(+), 74 deletions(-) create mode 100644 .changeset/renovate-8f23b96.md diff --git a/.changeset/renovate-8f23b96.md b/.changeset/renovate-8f23b96.md new file mode 100644 index 0000000000..b1dee297e2 --- /dev/null +++ b/.changeset/renovate-8f23b96.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend': patch +--- + +Updated dependency `google-auth-library` to `^9.0.0`. diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 675048e55e..219f16bc66 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -36,7 +36,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", - "google-auth-library": "^8.0.0" + "google-auth-library": "^9.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index cd1900037f..4954c64054 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -34,7 +34,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "google-auth-library": "^8.0.0", + "google-auth-library": "^9.0.0", "passport-google-oauth20": "^2.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 223f8758a7..3ec8c64fec 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -64,7 +64,7 @@ "express-promise-router": "^4.1.0", "express-session": "^1.17.1", "fs-extra": "^11.2.0", - "google-auth-library": "^8.0.0", + "google-auth-library": "^9.0.0", "jose": "^4.6.0", "knex": "^3.0.0", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 6280775f8b..b68bd27df6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4724,7 +4724,7 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/types": "workspace:^" express: ^4.18.2 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 languageName: unknown linkType: soft @@ -4770,7 +4770,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@types/passport-google-oauth20": ^2.0.3 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 passport-google-oauth20: ^2.0.0 supertest: ^6.1.3 languageName: unknown @@ -4962,7 +4962,7 @@ __metadata: express-promise-router: ^4.1.0 express-session: ^1.17.1 fs-extra: ^11.2.0 - google-auth-library: ^8.0.0 + google-auth-library: ^9.0.0 jose: ^4.6.0 knex: ^3.0.0 lodash: ^4.17.21 @@ -28056,13 +28056,6 @@ __metadata: languageName: node linkType: hard -"fast-text-encoding@npm:^1.0.0": - version: 1.0.3 - resolution: "fast-text-encoding@npm:1.0.3" - checksum: 3e51365896f06d0dcab128092d095a0037d274deec419fecbd2388bc236d7b387610e0c72f920c6126e00c885ab096fbfaa3645712f5b98f721bef6b064916a8 - languageName: node - linkType: hard - "fast-url-parser@npm:1.1.3, fast-url-parser@npm:^1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" @@ -28915,18 +28908,6 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^5.0.0, gaxios@npm:^5.0.1": - version: 5.0.1 - resolution: "gaxios@npm:5.0.1" - dependencies: - extend: ^3.0.2 - https-proxy-agent: ^5.0.0 - is-stream: ^2.0.0 - node-fetch: ^2.6.7 - checksum: 65464122a5be72084d07947536d18a0dcebd115b28cbcd19da8a98763a67c8c8fde995bf2f358251397c939df9d2ff84f54628a94f634a98a16c7b7553cdb4a5 - languageName: node - linkType: hard - "gaxios@npm:^6.0.0, gaxios@npm:^6.0.2": version: 6.1.1 resolution: "gaxios@npm:6.1.1" @@ -28939,16 +28920,6 @@ __metadata: languageName: node linkType: hard -"gcp-metadata@npm:^5.3.0": - version: 5.3.0 - resolution: "gcp-metadata@npm:5.3.0" - dependencies: - gaxios: ^5.0.0 - json-bigint: ^1.0.0 - checksum: 891ea0b902a17f33d7bae753830d23962b63af94ed071092c30496e7d26f8128ba9af43c3d38474bea29cb32a884b4bcb5720ce8b9de4a7e1108475d3d7ae219 - languageName: node - linkType: hard - "gcp-metadata@npm:^6.0.0": version: 6.1.0 resolution: "gcp-metadata@npm:6.1.0" @@ -29333,23 +29304,6 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^8.0.0": - version: 8.9.0 - resolution: "google-auth-library@npm:8.9.0" - dependencies: - arrify: ^2.0.0 - base64-js: ^1.3.0 - ecdsa-sig-formatter: ^1.0.11 - fast-text-encoding: ^1.0.0 - gaxios: ^5.0.0 - gcp-metadata: ^5.3.0 - gtoken: ^6.1.0 - jws: ^4.0.0 - lru-cache: ^6.0.0 - checksum: 8e0bc5f1e91804523786413bf4358e4c5ad94b1e873c725ddd03d0f1c242e2b38e26352c0f375334fbc1d94110f761b304aa0429de49b4a27ebc3875a5b56644 - languageName: node - linkType: hard - "google-auth-library@npm:^9.0.0": version: 9.2.0 resolution: "google-auth-library@npm:9.2.0" @@ -29383,17 +29337,6 @@ __metadata: languageName: node linkType: hard -"google-p12-pem@npm:^4.0.0": - version: 4.0.0 - resolution: "google-p12-pem@npm:4.0.0" - dependencies: - node-forge: ^1.3.1 - bin: - gp12-pem: build/src/bin/gp12-pem.js - checksum: f41a88d339e9fe633dc915bc0f3335c0196fa318f994dcd5dfaa0f3f7aa2d99f6122e2c80bd0f4bb22f2b61ff645b7cc782a74e12ceaf6c9ad9e08cdeb4d615e - languageName: node - linkType: hard - "google-protobuf@npm:^3.15.8, google-protobuf@npm:^3.19.1": version: 3.20.1 resolution: "google-protobuf@npm:3.20.1" @@ -29655,17 +29598,6 @@ __metadata: languageName: node linkType: hard -"gtoken@npm:^6.1.0": - version: 6.1.1 - resolution: "gtoken@npm:6.1.1" - dependencies: - gaxios: ^5.0.1 - google-p12-pem: ^4.0.0 - jws: ^4.0.0 - checksum: f063ed3f418f5a9c33fbe599b09f56a55b18f6c8d2913f11cc2fc5025d53b96fd6ab1b4deb2c7631167d6b89d0939e4ea77f94767fcf338862ffda8911250980 - languageName: node - linkType: hard - "gtoken@npm:^7.0.0": version: 7.0.1 resolution: "gtoken@npm:7.0.1" From c9e5b59f78db5f3bcdb6f3c3aeaf3723d63894ff Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 1 Feb 2024 16:26:14 +0100 Subject: [PATCH 054/483] errors: set statusCode in ResponseError Signed-off-by: Vincenzo Scamporlino --- .../errors/src/errors/ResponseError.test.ts | 2 ++ packages/errors/src/errors/ResponseError.ts | 26 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/errors/src/errors/ResponseError.test.ts b/packages/errors/src/errors/ResponseError.test.ts index 94f2e850d9..38db084b03 100644 --- a/packages/errors/src/errors/ResponseError.test.ts +++ b/packages/errors/src/errors/ResponseError.test.ts @@ -35,6 +35,8 @@ describe('ResponseError', () => { const e = await ResponseError.fromResponse(response as Response); expect(e.name).toEqual('ResponseError'); expect(e.message).toEqual('Request failed with 444 Fours'); + expect(e.statusCode).toEqual(444); + expect(e.statusText).toEqual('Fours'); expect(e.cause.name).toEqual('Fours'); expect(e.cause.message).toEqual('Expected fives'); expect(e.cause.stack).toEqual('lines'); diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts index f8ba0a1ba1..b5ca1e4ab8 100644 --- a/packages/errors/src/errors/ResponseError.ts +++ b/packages/errors/src/errors/ResponseError.ts @@ -53,6 +53,9 @@ export class ResponseError extends Error { */ readonly cause: Error; + readonly statusCode: number; + + readonly statusText: string; /** * Constructs a ResponseError based on a failed response. * @@ -65,9 +68,9 @@ export class ResponseError extends Error { ): Promise { const data = await parseErrorResponseBody(response); - const status = data.response.statusCode || response.status; - const statusText = data.error.name || response.statusText; - const message = `Request failed with ${status} ${statusText}`; + const statusCode = data.response.statusCode || response.status; + const statusText = response.statusText; + const message = `Request failed with ${statusCode} ${statusText}`; const cause = deserializeError(data.error); return new ResponseError({ @@ -75,19 +78,26 @@ export class ResponseError extends Error { response, data, cause, + statusCode, + statusText, }); } - private constructor(props: { + private constructor(opts: { message: string; response: ConsumedResponse; data: ErrorResponseBody; cause: Error; + statusCode: number; + statusText: string; }) { - super(props.message); + super(opts.message); + this.name = 'ResponseError'; - this.response = props.response; - this.body = props.data; - this.cause = props.cause; + this.response = opts.response; + this.body = opts.data; + this.cause = opts.cause; + this.statusCode = opts.statusCode; + this.statusText = opts.statusText; } } From b4cb0085b9ca7de247870f7d5b23f2ec4b4549ac Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 1 Feb 2024 16:26:29 +0100 Subject: [PATCH 055/483] backend-common: test for ResponseError Signed-off-by: Vincenzo Scamporlino --- .../src/middleware/errorHandler.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts index 3cc473daa0..ab07a91022 100644 --- a/packages/backend-common/src/middleware/errorHandler.test.ts +++ b/packages/backend-common/src/middleware/errorHandler.test.ts @@ -21,11 +21,13 @@ import { NotAllowedError, NotFoundError, NotModifiedError, + ResponseError, } from '@backstage/errors'; import express from 'express'; import createError from 'http-errors'; import request from 'supertest'; import { errorHandler } from './errorHandler'; +import { STATUS_CODES } from 'http'; describe('errorHandler', () => { it('gives default code and message', async () => { @@ -116,6 +118,53 @@ describe('errorHandler', () => { app.use('/ConflictError', () => { throw new ConflictError(); }); + app.use('/ResponseErrorBackstagePlugin', async (_req, _res, next) => { + const mockedResponse = { + status: jest.fn(() => mockedResponse), + json: jest.fn(() => mockedResponse), + } as unknown as jest.Mocked; + + // serialize AuthenticationError in mockedResponse + errorHandler()( + new AuthenticationError('an error'), + { method: 'GET', url: '' } as express.Request, + mockedResponse, + jest.fn(), + ); + + const status = mockedResponse.status.mock.calls[0][0]; + next( + await ResponseError.fromResponse({ + headers: new Headers({ + 'content-type': 'application/json', + }), + ok: false, + redirected: false, + status, + statusText: STATUS_CODES[status]!, + type: 'default', + url: '', + text: async () => + JSON.stringify(mockedResponse.json.mock.calls[0][0]), + }), + ); + }); + app.use('/ResponseError', async (_req, _res, next) => { + next( + await ResponseError.fromResponse({ + headers: new Headers({ + 'content-type': 'application/json', + }), + ok: false, + redirected: false, + status: 403, + statusText: STATUS_CODES[403]!, + type: 'default', + url: '', + text: async () => JSON.stringify({}), + }), + ); + }); app.use(errorHandler()); const r = request(app); @@ -138,6 +187,14 @@ describe('errorHandler', () => { expect((await r.get('/ConflictError')).body.error.name).toBe( 'ConflictError', ); + expect((await r.get('/ResponseErrorBackstagePlugin')).status).toBe(401); + expect((await r.get('/ResponseErrorBackstagePlugin')).body.error.name).toBe( + 'ResponseError', + ); + expect((await r.get('/ResponseError')).status).toBe(403); + expect((await r.get('/ResponseError')).body.error.name).toBe( + 'ResponseError', + ); }); it('logs all 500 errors', async () => { From 2636075b2fe50cbca9d3c7f3752f4e35eeb149ab Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 1 Feb 2024 18:16:25 +0100 Subject: [PATCH 056/483] ResponseError changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/cyan-dryers-share.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cyan-dryers-share.md diff --git a/.changeset/cyan-dryers-share.md b/.changeset/cyan-dryers-share.md new file mode 100644 index 0000000000..4c1ce1d553 --- /dev/null +++ b/.changeset/cyan-dryers-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': patch +--- + +Fixed an issue that was causing ResponseError not to report the HTTP status from the provided response. From f4cf3f3dd4a28cf567e0c3c383f66804bda92f6c Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:17:31 +0100 Subject: [PATCH 057/483] catalog-client: fix error message Signed-off-by: Vincenzo Scamporlino --- packages/catalog-client/src/CatalogClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index c36b3d96b5..81c0a6af5c 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -761,7 +761,7 @@ describe('CatalogClient', () => { }, 'url:http://example.com', ), - ).rejects.toThrow(/Request failed with 500 Error/); + ).rejects.toThrow(/Request failed with 500 Internal Server Error/); }); }); }); From 276781c2dd4fb52745dddf2e6a7a66294559ffed Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:25:01 +0100 Subject: [PATCH 058/483] vault: fix error message Signed-off-by: Vincenzo Scamporlino --- plugins/vault/src/api.test.ts | 2 +- .../src/components/EntityVaultTable/EntityVaultTable.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 7ecd09b6f4..777137d0e7 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -110,7 +110,7 @@ describe('api', () => { it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => { await expect(api.listSecrets('test/error')).rejects.toThrow( - 'Request failed with 400 Error', + 'Request failed with 400 Bad Request', ); }); }); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 5e191df9eb..14963ad13b 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -161,7 +161,7 @@ describe('EntityVaultTable', () => { expect( rendered.getByText( - /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/, + /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Bad Request/, ), ).toBeInTheDocument(); }); From 8a3932ffe8a93e3ca393e60d988a95d7ee80f24d Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:25:15 +0100 Subject: [PATCH 059/483] vault: fix warning in test Signed-off-by: Vincenzo Scamporlino --- .../EntityVaultCard/EntityVaultCard.test.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx index 9450ddc41d..ec62ea8757 100644 --- a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx +++ b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { ComponentEntity } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { EntityVaultCard } from './EntityVaultCard'; import { EntityProvider } from '@backstage/plugin-catalog-react'; @@ -45,8 +45,11 @@ describe('EntityVaultCard', () => { , ); - expect( - rendered.getByText(/Add the annotation to your Component YAML/), - ).toBeInTheDocument(); + + await waitFor(() => + expect( + rendered.getByText(/Add the annotation to your Component YAML/), + ).toBeInTheDocument(), + ); }); }); From b354046dadfe15e805ec45080406f26453e2ad6a Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:26:10 +0100 Subject: [PATCH 060/483] scaffolder-backend-module-confluence-to-markdown: fix error message Signed-off-by: Vincenzo Scamporlino --- .../src/actions/confluence/confluenceToMarkdown.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 5ce8bb2697..39c9c98544 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -221,7 +221,7 @@ describe('confluence:transform:markdown', () => { const action = createConfluenceToMarkdownAction(options); await expect(async () => { await action.handler(mockContext); - }).rejects.toThrow('Request failed with 401 Error'); + }).rejects.toThrow('Request failed with 401 nope'); }); it('should return nothing in results from the first api call and fail', async () => { @@ -284,6 +284,6 @@ describe('confluence:transform:markdown', () => { const action = createConfluenceToMarkdownAction(options); await expect(async () => { await action.handler(mockContext); - }).rejects.toThrow('Request failed with 404 Error'); + }).rejects.toThrow('Request failed with 404 nope'); }); }); From 20340074c47bad515df547798f0b5e6df1585d20 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:26:27 +0100 Subject: [PATCH 061/483] core-components: fix error text Signed-off-by: Vincenzo Scamporlino --- .../src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index 3f0c01b56b..05960cd213 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -97,7 +97,7 @@ describe('ProxiedSignInPage', () => { render(Subject); await expect( - screen.findByText('Request failed with 401 Error'), + screen.findByText('Request failed with 401 Unauthorized'), ).resolves.toBeInTheDocument(); }); }); From a2327acaab7b36953c2f3cd6204ba19bfc9a3d71 Mon Sep 17 00:00:00 2001 From: Vladimir Kobzev Date: Thu, 15 Feb 2024 13:33:56 +0100 Subject: [PATCH 062/483] add word-break: normal to the "moved in direction" table header cell Signed-off-by: Vladimir Kobzev --- .changeset/poor-beans-cross.md | 5 +++++ .../src/components/RadarTimeline/RadarTimeline.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-beans-cross.md diff --git a/.changeset/poor-beans-cross.md b/.changeset/poor-beans-cross.md new file mode 100644 index 0000000000..34c473a545 --- /dev/null +++ b/.changeset/poor-beans-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Fixed an issue with the "moved in direction" table header cell getting squished and becoming unreadable if a timeline description is too long diff --git a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx index e54c4a499e..2456c26918 100644 --- a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx +++ b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx @@ -47,7 +47,9 @@ const RadarTimeline = (props: Props): JSX.Element => { - Moved in direction + + Moved in direction + Moved to ring Moved on date Description From 406c5675a12f277984874e12a154f541c986ce09 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 13:41:55 +0100 Subject: [PATCH 063/483] errors: api report Signed-off-by: Vincenzo Scamporlino --- packages/errors/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index 3ece63035b..894aac8413 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -128,6 +128,10 @@ export class ResponseError extends Error { }, ): Promise; readonly response: ConsumedResponse; + // (undocumented) + readonly statusCode: number; + // (undocumented) + readonly statusText: string; } // @public From 8b7d574c3dffbd5286bb8eb530c3eef2328aa11e Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 9 Feb 2024 19:24:37 -0600 Subject: [PATCH 064/483] Unified Theme Docs Signed-off-by: Andre Wanlin --- docs/getting-started/app-custom-theme.md | 321 ++++++++++-------- packages/theme/api-report.md | 3 + .../theme/src/base/createBaseThemeOptions.ts | 76 +++-- packages/theme/src/base/index.ts | 5 +- 4 files changed, 222 insertions(+), 183 deletions(-) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index d6a13ca6e3..c70451df45 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -4,54 +4,38 @@ title: Customize the look-and-feel of your App description: Documentation on Customizing look and feel of the App --- -Backstage ships with a default theme with a light and dark mode variant. The -themes are provided as a part of the -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, -which also includes utilities for customizing the default theme, or creating -completely new themes. +Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes. ## Creating a Custom Theme -The easiest way to create a new theme is to use the `createTheme` function -exported by the -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You -can use it to override some basic parameters of the default theme such as the -color palette and font. +The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font. -For example, you can create a new theme based on the default light theme like -this: +For example, you can create a new theme based on the default light theme like this: ```ts -import { createTheme, lightTheme } from '@backstage/theme'; +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; -const myTheme = createTheme({ - palette: lightTheme.palette, +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), fontFamily: 'Comic Sans MS', defaultPageTheme: 'home', }); ``` -If you want more control over the theme, and for example customize font sizes -and margins, you can use the lower-level `createThemeOverrides` function -exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) -in combination with -[`createTheme`](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme) -from [`@material-ui/core`](https://www.npmjs.com/package/@material-ui/core). See -the "Overriding Backstage and Material UI css rules" section below. - -You can also create a theme from scratch that matches the `BackstageTheme` type -exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). -See the -[Material UI docs on theming](https://material-ui.com/customization/theming/) -for more information about how that can be done. +You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the +[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. ## Using your Custom Theme -To add a custom theme to your Backstage app, you pass it as configuration to -`createApp`. +To add a custom theme to your Backstage app, you pass it as configuration to `createApp`. -For example, adding the theme that we created in the previous section can be -done like this: +For example, adding the theme that we created in the previous section can be done like this: ```tsx import { createApp } from '@backstage/app-defaults'; @@ -68,70 +52,68 @@ const app = createApp({ variant: 'light', icon: , Provider: ({ children }) => ( - - {children} - + ), }] }) ``` -Note that your list of custom themes overrides the default themes. If you still -want to use the default themes, they are exported as `lightTheme` and -`darkTheme` from -[`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). +Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.light` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). ## Example of a custom theme ```ts import { - createTheme, + createBaseThemeOptions, + createUnifiedTheme, genPageTheme, - lightTheme, + palettes, shapes, } from '@backstage/theme'; -const myTheme = createTheme({ - palette: { - ...lightTheme.palette, - primary: { - main: '#343b58', +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: { + ...palettes.light, + primary: { + main: '#343b58', + }, + secondary: { + main: '#565a6e', + }, + error: { + main: '#8c4351', + }, + warning: { + main: '#8f5e15', + }, + info: { + main: '#34548a', + }, + success: { + main: '#485e30', + }, + background: { + default: '#d5d6db', + paper: '#d5d6db', + }, + banner: { + info: '#34548a', + error: '#8c4351', + text: '#343b58', + link: '#565a6e', + }, + errorBackground: '#8c4351', + warningBackground: '#8f5e15', + infoBackground: '#343b58', + navigation: { + background: '#343b58', + indicator: '#8f5e15', + color: '#d5d6db', + selectedColor: '#ffffff', + }, }, - secondary: { - main: '#565a6e', - }, - error: { - main: '#8c4351', - }, - warning: { - main: '#8f5e15', - }, - info: { - main: '#34548a', - }, - success: { - main: '#485e30', - }, - background: { - default: '#d5d6db', - paper: '#d5d6db', - }, - banner: { - info: '#34548a', - error: '#8c4351', - text: '#343b58', - link: '#565a6e', - }, - errorBackground: '#8c4351', - warningBackground: '#8f5e15', - infoBackground: '#343b58', - navigation: { - background: '#343b58', - indicator: '#8f5e15', - color: '#d5d6db', - selectedColor: '#ffffff', - }, - }, + }), defaultPageTheme: 'home', fontFamily: 'Comic Sans MS', /* below drives the header colors */ @@ -161,16 +143,92 @@ const myTheme = createTheme({ }); ``` -For a more complete example of a custom theme including Backstage and -Material UI component overrides, see the [Aperture -theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) -from the [Backstage demo site](https://demo.backstage.io). +For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). + +## Custom Typography + +When creating a custom theme you can also customize vairous aspexts of the default typography, here's an exampl using simplified theme: + +```tsx +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + htmlFontSize: 16, + fontFamily: 'Arial, sans-serif', + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: + +```tsx +import { + createBaseThemeOptions, + createUnifiedTheme, + defaultTypography, + palettes, +} from '@backstage/theme'; + +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` ## Overriding Backstage and Material UI components styles -When creating a custom theme you would be applying different values to -component's css rules that use the theme object. For example, a Backstage -component's styles might look like this: +When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: ```tsx const useStyles = makeStyles( @@ -185,83 +243,50 @@ const useStyles = makeStyles( ); ``` -Notice how the `padding` is getting its value from `theme.spacing`, that means -that setting a value for spacing in your custom theme would affect this -component padding property and the same goes for `backgroundImage` which uses -`theme.page.backgroundImage`. However, the `boxShadow` property doesn't -reference any value from the theme, that means that creating a custom theme -wouldn't be enough to alter the `box-shadow` property or to add css rules that -aren't already defined like a margin. For these cases you should also create an -override. +Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. + +Here's how you would do that: ```tsx -import { createApp } from '@backstage/core-app-api'; -import { BackstageTheme, lightTheme } from '@backstage/theme'; -/** - * The `@backstage/core-components` package exposes this type that - * contains all Backstage and `material-ui` components that can be - * overridden along with the classes key those components use. - */ -import { BackstageOverrides } from '@backstage/core-components'; +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; -export const createCustomThemeOverrides = ( - theme: BackstageTheme, -): BackstageOverrides => { - return { +const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', + components: { BackstageHeader: { - header: { - width: 'auto', - margin: '20px', - boxShadow: 'none', - borderBottom: `4px solid ${theme.palette.primary.main}`, + styleOverrides: { + header: ({ theme }) => ({ + width: 'auto', + margin: '20px', + boxShadow: 'none', + borderBottom: `4px solid ${theme.palette.primary.main}`, + }), }, }, - }; -}; - -const customTheme: BackstageTheme = { - ...lightTheme, - overrides: { - // These are the overrides that Backstage applies to `material-ui` components - ...lightTheme.overrides, - // These are your custom overrides, either to `material-ui` or Backstage components. - ...createCustomThemeOverrides(lightTheme), }, -}; - -const app = createApp({ - apis: ..., - plugins: ..., - themes: [{ - id: 'my-theme', - title: 'My Custom Theme', - variant: 'light', - Provider: ({ children }) => ( - - {children} - - ), - }] }); ``` ## Custom Logo -In addition to a custom theme, you can also customize the logo displayed at the -far top left of the site. +In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. -In your frontend app, locate `src/components/Root/` folder. You'll find two -components: +In your frontend app, locate `src/components/Root/` folder. You'll find two components: - `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. -- `LogoIcon.tsx` - A smaller logo used when the sidebar navigation is closed. +- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. -To replace the images, you can simply replace the relevant code in those -components with raw SVG definitions. +To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. -You can also use another web image format such as PNG by importing it. To do -this, place your new image into a new subdirectory such as -`src/components/Root/logo/my-company-logo.png`, and then add this code: +You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: ```tsx import MyCustomLogoFull from './logo/my-company-logo.png'; diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 21d6be6a78..988569ece7 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -205,6 +205,9 @@ export const darkTheme: Theme_3; // @public export const defaultComponentThemes: ThemeOptions['components']; +// @public +export const defaultTypography: BackstageTypography; + // @public export function genPageTheme(props: { colors: string[]; diff --git a/packages/theme/src/base/createBaseThemeOptions.ts b/packages/theme/src/base/createBaseThemeOptions.ts index f0c8ab5fe9..ab02af61c8 100644 --- a/packages/theme/src/base/createBaseThemeOptions.ts +++ b/packages/theme/src/base/createBaseThemeOptions.ts @@ -22,6 +22,46 @@ const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; const DEFAULT_PAGE_THEME = 'home'; +/** + * Default Typography settings. + * + * @public + */ +export const defaultTypography: BackstageTypography = { + htmlFontSize: DEFAULT_HTML_FONT_SIZE, + fontFamily: DEFAULT_FONT_FAMILY, + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, +}; + /** * Options for {@link createBaseThemeOptions}. * @@ -57,40 +97,8 @@ export function createBaseThemeOptions( throw new Error(`${defaultPageTheme} is not defined in pageTheme.`); } - const defaultTypography: BackstageTypography = { - htmlFontSize, - fontFamily, - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h5: { - fontWeight: 700, - fontSize: 24, - marginBottom: 4, - }, - h6: { - fontWeight: 700, - fontSize: 20, - marginBottom: 2, - }, - }; + defaultTypography.htmlFontSize = htmlFontSize; + defaultTypography.fontFamily = fontFamily; return { palette, diff --git a/packages/theme/src/base/index.ts b/packages/theme/src/base/index.ts index 2da9fb0fac..f7a6b2fd07 100644 --- a/packages/theme/src/base/index.ts +++ b/packages/theme/src/base/index.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -export { createBaseThemeOptions } from './createBaseThemeOptions'; +export { + createBaseThemeOptions, + defaultTypography, +} from './createBaseThemeOptions'; export type { BaseThemeOptionsInput } from './createBaseThemeOptions'; export { colorVariants, genPageTheme, pageTheme, shapes } from './pageTheme'; export { palettes } from './palettes'; From 6f4d2a0cbb6821af5f540126686aee5d391c5136 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 9 Feb 2024 19:26:32 -0600 Subject: [PATCH 065/483] Added changeset Signed-off-by: Andre Wanlin --- .changeset/fifty-moons-study.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fifty-moons-study.md diff --git a/.changeset/fifty-moons-study.md b/.changeset/fifty-moons-study.md new file mode 100644 index 0000000000..3744960870 --- /dev/null +++ b/.changeset/fifty-moons-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Exported `defaultTypography` to make adjusting these values in a custom theme easier From ee35f26d72bf1fd2737ab5b42ac161996e6588d4 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 10 Feb 2024 14:49:12 -0600 Subject: [PATCH 066/483] Fixed typos Signed-off-by: Andre Wanlin --- docs/getting-started/app-custom-theme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index c70451df45..766b9ed1e8 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -147,7 +147,7 @@ For a more complete example of a custom theme including Backstage and Material U ## Custom Typography -When creating a custom theme you can also customize vairous aspexts of the default typography, here's an exampl using simplified theme: +When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: ```tsx import { From 3e8a02947ddba8e74fb8e640c43f8e7be045405f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 15 Feb 2024 08:25:53 -0600 Subject: [PATCH 067/483] Refinements based on recent Discord comments Signed-off-by: Andre Wanlin --- docs/getting-started/app-custom-theme.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 766b9ed1e8..6ce70be063 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -12,7 +12,7 @@ The easiest way to create a new theme is to use the `createUnifiedTheme` functio For example, you can create a new theme based on the default light theme like this: -```ts +```ts title="packages/app/src/theme/myTheme.ts" import { createBaseThemeOptions, createUnifiedTheme, @@ -28,6 +28,8 @@ const myTheme = createUnifiedTheme({ }); ``` +> Note: we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized. + You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the [Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. @@ -37,7 +39,7 @@ To add a custom theme to your Backstage app, you pass it as configuration to `cr For example, adding the theme that we created in the previous section can be done like this: -```tsx +```tsx title="packages/app/src/App.tsx" import { createApp } from '@backstage/app-defaults'; import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; @@ -62,7 +64,7 @@ Note that your list of custom themes overrides the default themes. If you still ## Example of a custom theme -```ts +```ts title="packages/app/src/theme/myTheme.ts" import { createBaseThemeOptions, createUnifiedTheme, @@ -149,7 +151,7 @@ For a more complete example of a custom theme including Backstage and Material U When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: -```tsx +```ts title="packages/app/src/theme/myTheme.ts" import { createBaseThemeOptions, createUnifiedTheme, @@ -200,7 +202,7 @@ const myTheme = createUnifiedTheme({ If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: -```tsx +```ts title="packages/app/src/theme/myTheme.ts" import { createBaseThemeOptions, createUnifiedTheme, @@ -247,7 +249,7 @@ Notice how the `padding` is getting its value from `theme.spacing`, that means t Here's how you would do that: -```tsx +```ts title="packages/app/src/theme/myTheme.ts" import { createBaseThemeOptions, createUnifiedTheme, @@ -433,7 +435,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu: 3. Then update the `@backstage/core-components` import like this: - ```tsx + ```tsx title="packages/app/src/components/Root/Root.tsx" import { Sidebar, sidebarConfig, @@ -455,7 +457,7 @@ For this example we'll show you how you can expand the sidebar with a sub-menu: 4. Finally replace `` with this: - ```tsx + ```tsx title="packages/app/src/components/Root/Root.tsx" Date: Thu, 15 Feb 2024 18:17:15 +0000 Subject: [PATCH 068/483] Added Azure Devops Scopes to scm api Signed-off-by: Phill Morton --- .changeset/forty-oranges-joke.md | 5 +++++ packages/integration-react/src/api/ScmAuth.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/forty-oranges-joke.md diff --git a/.changeset/forty-oranges-joke.md b/.changeset/forty-oranges-joke.md new file mode 100644 index 0000000000..b2fdaccdc1 --- /dev/null +++ b/.changeset/forty-oranges-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-react': patch +--- + +Updated azure devops scopes to include the clientid for Azure Dev Ops OAuth. diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index b3dab4029e..a22b15e400 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -199,13 +199,13 @@ export class ScmAuth implements ScmAuthApi { const host = options?.host ?? 'dev.azure.com'; return new ScmAuth('azure', microsoftAuthApi, host, { default: [ - 'vso.build', - 'vso.code', - 'vso.graph', - 'vso.project', - 'vso.profile', + '499b84ac-1321-427f-aa17-267ca6975798/vso.build', + '499b84ac-1321-427f-aa17-267ca6975798/vso.code', + '499b84ac-1321-427f-aa17-267ca6975798/vso.graph', + '499b84ac-1321-427f-aa17-267ca6975798/vso.project', + '499b84ac-1321-427f-aa17-267ca6975798/vso.profile', ], - repoWrite: ['vso.code_manage'], + repoWrite: ['499b84ac-1321-427f-aa17-267ca6975798/vso.code_manage'], }); } From b38dc5591a924a4614c714c5ee7d16d7fd27ff1a Mon Sep 17 00:00:00 2001 From: Phill Morton Date: Thu, 15 Feb 2024 18:17:15 +0000 Subject: [PATCH 069/483] Added Azure Devops Scopes to scm api Signed-off-by: Phill Morton --- .changeset/forty-oranges-joke.md | 5 +++++ packages/integration-react/src/api/ScmAuth.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/forty-oranges-joke.md diff --git a/.changeset/forty-oranges-joke.md b/.changeset/forty-oranges-joke.md new file mode 100644 index 0000000000..b2fdaccdc1 --- /dev/null +++ b/.changeset/forty-oranges-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-react': patch +--- + +Updated azure devops scopes to include the clientid for Azure Dev Ops OAuth. diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index b3dab4029e..a22b15e400 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -199,13 +199,13 @@ export class ScmAuth implements ScmAuthApi { const host = options?.host ?? 'dev.azure.com'; return new ScmAuth('azure', microsoftAuthApi, host, { default: [ - 'vso.build', - 'vso.code', - 'vso.graph', - 'vso.project', - 'vso.profile', + '499b84ac-1321-427f-aa17-267ca6975798/vso.build', + '499b84ac-1321-427f-aa17-267ca6975798/vso.code', + '499b84ac-1321-427f-aa17-267ca6975798/vso.graph', + '499b84ac-1321-427f-aa17-267ca6975798/vso.project', + '499b84ac-1321-427f-aa17-267ca6975798/vso.profile', ], - repoWrite: ['vso.code_manage'], + repoWrite: ['499b84ac-1321-427f-aa17-267ca6975798/vso.code_manage'], }); } From 61f7a1911b36dd523a27297a5210f9093441b4a2 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Thu, 15 Feb 2024 21:04:09 +0100 Subject: [PATCH 070/483] fixed Azure DevOps ADR file path Signed-off-by: Deepankumar Loganathan --- plugins/adr/src/components/AdrReader/AdrReader.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx index 6b07eee4cd..f41b1b1fdb 100644 --- a/plugins/adr/src/components/AdrReader/AdrReader.tsx +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -45,8 +45,18 @@ export const AdrReader = (props: { const scmIntegrations = useApi(scmIntegrationsApiRef); const adrApi = useApi(adrApiRef); const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); + let url = `${adrLocationUrl.replace(/\/$/, '')}`; + const adrUrlPath = url.match(/path=\/.*\&/); + if (adrUrlPath) { + // Azure DevOps SCM handle the path in URL Params + const adrPath = adrUrlPath![0].replace(/\&$/, ''); + const regex = new RegExp(`${adrPath}`); + url = url.replace(regex, `${adrPath}/${adr}}`); + } else { + // Other SCM tools + url = `${url}/${adr}`; + } - const url = `${adrLocationUrl.replace(/\/$/, '')}/${adr}`; const { value, loading, error } = useAsync( async () => adrApi.readAdr(url), [url], From f9b9926f4d8a4fc81c70ea8543ec7108cafe8f2b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 15 Feb 2024 22:36:25 +0100 Subject: [PATCH 071/483] docs: add new community plugins project area Signed-off-by: Vincenzo Scamporlino --- OWNERS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OWNERS.md b/OWNERS.md index 1e92e68c40..b8d317e2d3 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -108,6 +108,20 @@ Scope: The TechDocs plugin and related tooling These incubating project areas have shared ownership with @backstage/maintainers. +### Community Plugins + +Team: @backstage/community-plugins-maintainers + +Scope: Tooling related to the Backstage [Community Plugins repository](https://github.com/backstage/community-plugins) + +| Name | Organization | GitHub | Discord | +| -------------------- | ------------ | ------------------------------------------- | ------------ | +| Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` | +| Tomas Kral | Red Hat | [kadel](https://github.com/kadel) | `tomkral` | +| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Philipp Hugenroth | Spotify | [tudi2d](https://github.com/tudi2d) | `tudi2d` | +| Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | + ### OpenAPI Tooling Team: @backstage/openapi-tooling-maintainers From 85ec23ebad7d641a50949d970d44bd5a4d039f5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:13:00 +0000 Subject: [PATCH 072/483] fix(deps): update dependency json-schema-to-ts to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-0300bde.md | 5 +++++ packages/backend-openapi-utils/package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/renovate-0300bde.md diff --git a/.changeset/renovate-0300bde.md b/.changeset/renovate-0300bde.md new file mode 100644 index 0000000000..71ed1df83c --- /dev/null +++ b/.changeset/renovate-0300bde.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Updated dependency `json-schema-to-ts` to `^3.0.0`. diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 2a8ff3bb30..c6b88dc0ad 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -44,7 +44,7 @@ "express": "^4.17.1", "express-openapi-validator": "^5.0.4", "express-promise-router": "^4.1.0", - "json-schema-to-ts": "^2.6.2", + "json-schema-to-ts": "^3.0.0", "lodash": "^4.17.21", "openapi-merge": "^1.3.2", "openapi3-ts": "^3.1.2" diff --git a/yarn.lock b/yarn.lock index c14cb29002..11c2eb74a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3418,7 +3418,7 @@ __metadata: express: ^4.17.1 express-openapi-validator: ^5.0.4 express-promise-router: ^4.1.0 - json-schema-to-ts: ^2.6.2 + json-schema-to-ts: ^3.0.0 lodash: ^4.17.21 openapi-merge: ^1.3.2 openapi3-ts: ^3.1.2 @@ -32722,14 +32722,14 @@ __metadata: languageName: node linkType: hard -"json-schema-to-ts@npm:^2.6.2": - version: 2.12.0 - resolution: "json-schema-to-ts@npm:2.12.0" +"json-schema-to-ts@npm:^3.0.0": + version: 3.0.0 + resolution: "json-schema-to-ts@npm:3.0.0" dependencies: "@babel/runtime": ^7.18.3 "@types/json-schema": ^7.0.9 ts-algebra: ^1.2.2 - checksum: 6dc4bc836591d888beb20e8bf45dfa3b75df4a331f18675bd2e39a2262e105e0dc86012031fff0be408499a07ae8bc30f5a879c24696189b2c30a34edd5fa72f + checksum: 4f33a0fee49cc058b16b4e4effd83cfb0c6000c6916990fda61b3622fa7967c9edbad59e34991eb869ebcd4989383d4c72f99a51a77163ac47f80da2c57d2aa8 languageName: node linkType: hard From 533563474d98fecd3615fd2f2442a0aea112f3ab Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Fri, 16 Feb 2024 09:35:35 +0100 Subject: [PATCH 073/483] changeset updated Signed-off-by: Deepankumar Loganathan --- .changeset/clever-eagles-boil.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clever-eagles-boil.md diff --git a/.changeset/clever-eagles-boil.md b/.changeset/clever-eagles-boil.md new file mode 100644 index 0000000000..6e69d2db2f --- /dev/null +++ b/.changeset/clever-eagles-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +Fixed Azure DevOps ADR file path From 526f00a9bee4337eb77a880a8f3b54692bc4805d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 14 Feb 2024 12:56:46 +0100 Subject: [PATCH 074/483] docs(org): create alpha readme file Signed-off-by: Camila Belo --- .changeset/nervous-lions-suffer.md | 5 + packages/app-next/app-config.yaml | 1 - plugins/org/OrgGroupProfileEntityCard.png | Bin 0 -> 25091 bytes plugins/org/OrgMembersListCard.png | Bin 0 -> 48346 bytes plugins/org/OrgOwnershipCard.png | Bin 0 -> 118385 bytes plugins/org/OrgUserProfileEntityCard.png | Bin 0 -> 16209 bytes plugins/org/README-alpha.md | 462 ++++++++++++++++++++++ plugins/org/README.md | 3 + 8 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 .changeset/nervous-lions-suffer.md create mode 100644 plugins/org/OrgGroupProfileEntityCard.png create mode 100644 plugins/org/OrgMembersListCard.png create mode 100644 plugins/org/OrgOwnershipCard.png create mode 100644 plugins/org/OrgUserProfileEntityCard.png create mode 100644 plugins/org/README-alpha.md diff --git a/.changeset/nervous-lions-suffer.md b/.changeset/nervous-lions-suffer.md new file mode 100644 index 0000000000..b3a0400c26 --- /dev/null +++ b/.changeset/nervous-lions-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Document the new frontend system extensions for the org plugin. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 408b08acee..8bfd79be54 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -22,7 +22,6 @@ app: - entity-card:catalog-graph/relations: config: height: 300 - - entity-card:azure-devops/readme - entity-card:api-docs/has-apis - entity-card:api-docs/consumed-apis - entity-card:api-docs/provided-apis diff --git a/plugins/org/OrgGroupProfileEntityCard.png b/plugins/org/OrgGroupProfileEntityCard.png new file mode 100644 index 0000000000000000000000000000000000000000..c012b54d04bdceb776edcc8579e4654a9d410fa4 GIT binary patch literal 25091 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

yJPQ03ySfc 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 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgMembersListCard.png b/plugins/org/OrgMembersListCard.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4d2b073127401bf57f7d0b028972d5a6ebab11 GIT binary patch literal 48346 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 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgOwnershipCard.png b/plugins/org/OrgOwnershipCard.png new file mode 100644 index 0000000000000000000000000000000000000000..f9358a65d1b2de5b9afb532a50e8f8785f5e8c5b GIT binary patch literal 118385 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�?^v)$kUsg>&jaiG-^+hZ|+RyyY)F~^8 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⪻@(g?`0K{%>k1FIQlj5!jT*F~L|nYN8Ds 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 IE&G@M2jiSEuK)l5 literal 0 HcmV?d00001 diff --git a/plugins/org/OrgUserProfileEntityCard.png b/plugins/org/OrgUserProfileEntityCard.png new file mode 100644 index 0000000000000000000000000000000000000000..d163243ef1a25ef2759ebde889009cfebc6ae6b3 GIT binary patch literal 16209 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 A-~a#s literal 0 HcmV?d00001 diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md new file mode 100644 index 0000000000..a7e1a071a9 --- /dev/null +++ b/plugins/org/README-alpha.md @@ -0,0 +1,462 @@ +# Org Plugin + +> [!WARNING] +> This documentation is made for those using the experimental new Frontend system. +> If you are not using the new frontend system, please go [here](./README.md). + +This is a plugin that extends the Catalog entity page with some users and groups overview cards: + +- Group Profile Entity Card +- Member List Entity Card +- Ownership Entity Card +- User Profile Entity Card + +Here is a Catalog group page showing the group profile, members, and ownership cards: + +![Group Page example](./docs/group-page-example.png) + +And below is an example of how a user page looks with the user profile and ownership cards: + +![Group Page example](./docs/user-profile-example.png) + +## Table of Content + +- [Installation](#installation) +- [Packages](#packages) +- [Routes](#routes) +- [Extensions](#extensions) + - [My Groups Sidebar Item](#my-groups-sidebar-item) + - [Entity Group Profile Card](#entity-group-profile-card) + - [Entity Group Profile Card](#entity-members-list-card) + - [Entity Group Profile Card](#entity-members-list-card) + - [Entity Group Profile Card](#entity-user-profile-card) + +## Installation + +1. Install the `org` plugin in you Backstage app: + + ```bash + # From your Backstage root directory + yarn --cwd packages/app add @backstage/plugin-org + ``` + +2. Enable which entity cards and tabs you would like to see on the catalog entity page: + + ```yaml + # app-config.yaml + app: + experimental: + # Auto discovering all plugins extensions + packages: all + extensions: + # Enabling the org plugin cards + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile + ``` + +3. Then start the app, navigate to an entity's page and see the cards and contents in there. + +## Packages + +The `org` plugin can be automatically discovered, and it is also possible to enable it only in certain [environments](https://backstage.io/docs/conf/writing/#configuration-files). See [this](https://backstage.io/docs/frontend-system/architecture/app/#feature-discovery) packages documentation for more details. + +## Routes + +The `org` plugin exposes a external route that can be used to configure route bindings. + +| Key | Type | Description | +| -------------- | -------------- | ---------------------------------- | +| `catalogIndex` | External route | A route ref to Catalog Index page. | + +As an example, here is an association between the external catalog index page and a regular route from another plugin: + +```yaml +# app-config.yaml +app: + routes: + bindings: + # example binding org and catalog index pages + org.catalogIndex: catalog.catalogIndex +``` + +Route binding is also possible through code. For more information, see [this](https://backstage.io/docs/frontend-system/architecture/routes#binding-external-route-references) documentation. + +## Extensions + +### My Groups Sidebar Item + +As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. + +> [!IMPORTANT] +> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. + +```tsx +// ... +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; + +export default createExtensionOverrides({ + extensions: [ + createExtension({ + // These namespace and name are necessary so the system knows that this extension will override the default app nav extension + namespace: 'app', + name: 'nav', + // Keeping the same attachment point as in the default App/Nav extension + attachTo: { id: 'app/layout', input: 'nav' }, + // Keeping the same inputs as in the default App/Nav extension + inputs: { + items: createExtensionInput({ + target: createNavItemExtension.targetDataRef, + }), + logos: createExtensionInput( + { + elements: createNavLogoExtension.logoElementsDataRef, + }, + { + singleton: true, + optional: true, + }, + ), + }, + // Keeping the same output as in the default App/Nav extension + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {/* Code borrowed from the default extension implementation to render the logos and items inputs */} + + + {inputs.items.map((item, index) => ( + + ))} + {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} + }> + {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} + + + + ), + }; + }, + }), + ], +}); +``` + +### Entity Cards + +The `org` plugin provide some entity cards you can enable to customize the Software Catalog entity page. + +> [!IMPORTANT] +> The order in which cards are listed in the configuration file will determine the order in which they appear in overview cards and tab lists on entity pages. + +See a complete cards list below: + +#### Entity Group Profile Card + +This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view, edit, or update groups metadata, such as team avatar, name, email, parent, and child groups. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | --------------- | ------------------------------- | ----------------------------------------------------------------------------------------- | +| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org group profile entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/group-profile: false + # or + # - entity-card:org/group-profile: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `group-profile` overview card only for entities of kind group: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/group-profile: + config: + # The default value is "kind:group" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group' +``` + +##### Override + +Use extension overrides for completely re-implementing the group-profile entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'group-profile', + // By default, this card will show up only for groups + filter: 'kind:group' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity Members List Card + +An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays the names and emails of group members. By clicking the member's name, you'll be directed to the user's catalog page, and the email opens your default email program. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------- | +| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org members list entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/members-list: false + # or + # - entity-card:org/members-list: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `members-list` overview card only for entities of kind group: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/members-list: + config: + # The default value is "kind:group" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group' +``` + +##### Override + +Use extension overrides for completely re-implementing the members-list entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'members-list', + // By default, this card will show up only for groups + filter: 'kind:group' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity Ownership Card + +An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays direct or aggregated group or user ownership relationships. Each entity listed in the card links to its respective entity page in the catalog. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | ----------- | --------------------------- | -------------------------------------------------------------------------------- | +| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org members list entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/ownership: false + # or + # - entity-card:org/ownership: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `ownership` overview card only for entities of kind group or user: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "group" or "user" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/ownership: + config: + # The default value is "kind:group,user" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:group,user' +``` + +##### Override + +Use extension overrides for completely re-implementing the ownership entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'ownership', + // By default, this card will show up only for groups or users + filter: 'kind:group,user' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +#### Entity User Profile Card + +This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view user metadata including avatar, name, email, and team. Clicking on the email link will open your default email program while clicking on the team link will direct you to the team page in the catalog plugin. + +| Kind | Namespace | Name | Id | Example | +| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------- | +| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | Entity Group Profile Card | + +##### Disable + +This card is disabled by default when you install the `org` plugin, but to ensure the card will always be disabled or enabled regardless of the extension's default definition, add the following configuration: + +```yaml +# app-config.yaml +# example disabling the org user profile entity card extension +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + # use false as value for disabling the extension and true for enabling + - entity-card:org/user-profile: false + # or + # - entity-card:org/user-profile: + # - config: + # # set 'true' for enabling it again + # disabled: true +``` + +##### Config + +There is only one configuration available for this entity card extension, which is setting an entity filter that determines when the card should be displayed on the entity page. + +Here is an example showing the `user-profile` overview card only for entities of kind group or user: + +```yaml +# app-config.yaml +# example setting the extension to only show up for entities with kind "user" +app: + extensions: + # this is the extension id and it follows the naming pattern bellow: + # /: + - entity-card:org/user-profile: + config: + # The default value is "kind:user" + # For more information about entity cards filters, check out this pull request + # https://github.com/backstage/backstage/pull/21480 + filter: 'kind:user' +``` + +##### Override + +Use extension overrides for completely re-implementing the user-profile entity card extension: + +```tsx +import { createExtensionOverrides } from '@backstage/backstage-plugin-api'; +import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; + +export default createExtensionOverrides({ + extensions: [ + createEntityCardExtension({ + // These namespace and name necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin + namespace: 'org', + name: 'user-profile', + // By default, this card will show up only for groups or users + filter: 'kind:user' + // Returing a custom card component + loader: () => + import('./components').then(m => ), + }), + ], +}); +``` + +For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). diff --git a/plugins/org/README.md b/plugins/org/README.md index 0c5452aa62..9cf165f06d 100644 --- a/plugins/org/README.md +++ b/plugins/org/README.md @@ -1,5 +1,8 @@ # Org Plugin for Backstage +> Disclaimer: +> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). + ## Features - Show Group Page From 540d4820ac10eb5fbdbfb7f931a99a1928d3f689 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 14 Feb 2024 15:11:01 +0100 Subject: [PATCH 075/483] refactor(org): apply review suggestions Signed-off-by: Camila Belo --- plugins/org/README-alpha.md | 144 ++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md index a7e1a071a9..35d4342070 100644 --- a/plugins/org/README-alpha.md +++ b/plugins/org/README-alpha.md @@ -25,11 +25,11 @@ And below is an example of how a user page looks with the user profile and owner - [Packages](#packages) - [Routes](#routes) - [Extensions](#extensions) - - [My Groups Sidebar Item](#my-groups-sidebar-item) - [Entity Group Profile Card](#entity-group-profile-card) - [Entity Group Profile Card](#entity-members-list-card) - [Entity Group Profile Card](#entity-members-list-card) - [Entity Group Profile Card](#entity-user-profile-card) + - [My Groups Sidebar Item](#my-groups-sidebar-item) ## Installation @@ -85,73 +85,6 @@ Route binding is also possible through code. For more information, see [this](ht ## Extensions -### My Groups Sidebar Item - -As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. - -> [!IMPORTANT] -> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. - -```tsx -// ... -import { MyGroupsSidebarItem } from '@backstage/plugin-org'; -import GroupIcon from '@material-ui/icons/People'; - -export default createExtensionOverrides({ - extensions: [ - createExtension({ - // These namespace and name are necessary so the system knows that this extension will override the default app nav extension - namespace: 'app', - name: 'nav', - // Keeping the same attachment point as in the default App/Nav extension - attachTo: { id: 'app/layout', input: 'nav' }, - // Keeping the same inputs as in the default App/Nav extension - inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, - }), - logos: createExtensionInput( - { - elements: createNavLogoExtension.logoElementsDataRef, - }, - { - singleton: true, - optional: true, - }, - ), - }, - // Keeping the same output as in the default App/Nav extension - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - {/* Code borrowed from the default extension implementation to render the logos and items inputs */} - - - {inputs.items.map((item, index) => ( - - ))} - {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} - }> - {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} - - - - ), - }; - }, - }), - ], -}); -``` - ### Entity Cards The `org` plugin provide some entity cards you can enable to customize the Software Catalog entity page. @@ -221,7 +154,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'group-profile' entity card extension provided by the 'org' plugin namespace: 'org', name: 'group-profile', // By default, this card will show up only for groups @@ -296,7 +229,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'members-list' entity card extension provided by the 'org' plugin namespace: 'org', name: 'members-list', // By default, this card will show up only for groups @@ -371,7 +304,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'ownership' entity card extension provided by the 'org' plugin namespace: 'org', name: 'ownership', // By default, this card will show up only for groups or users @@ -446,7 +379,7 @@ import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha export default createExtensionOverrides({ extensions: [ createEntityCardExtension({ - // These namespace and name necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin + // These namespace and name are necessary so the system knows that this extension will override the default 'user-profile' entity card extension provided by the 'org' plugin namespace: 'org', name: 'user-profile', // By default, this card will show up only for groups or users @@ -460,3 +393,70 @@ export default createExtensionOverrides({ ``` For more information about where to place extension overrides, see the official [documentation](https://backstage.io/docs/frontend-system/architecture/extension-overrides). + +### My Groups Sidebar Item + +As the [NavItem](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) extension type does not support conditional rendering, this plugin does not provide navigation items, so to use the `MyGroupsSidebarItem` component, we recommend overriding the [App/Nav](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions#app-nav) extension and adding the item statically. + +> [!IMPORTANT] +> As you can see in the example below, we are using the same attachment point, inputs and outputs as the default App/Nav extension to avoid side effects on the NavItem and NavLogo extensions. + +```tsx +// ... +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; + +export default createExtensionOverrides({ + extensions: [ + createExtension({ + // These namespace and name are necessary so the system knows that this extension will override the default app nav extension + namespace: 'app', + name: 'nav', + // Keeping the same attachment point as in the default App/Nav extension + attachTo: { id: 'app/layout', input: 'nav' }, + // Keeping the same inputs as in the default App/Nav extension + inputs: { + items: createExtensionInput({ + target: createNavItemExtension.targetDataRef, + }), + logos: createExtensionInput( + { + elements: createNavLogoExtension.logoElementsDataRef, + }, + { + singleton: true, + optional: true, + }, + ), + }, + // Keeping the same output as in the default App/Nav extension + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {/* Code borrowed from the default extension implementation to render the logos and items inputs */} + + + {inputs.items.map((item, index) => ( + + ))} + {/* Here is where we actually modifies the default implementation by adding a static item to render a group of squad pages */} + }> + {/* The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. */} + + + + ), + }; + }, + }), + ], +}); +``` From 0b1e0b1028a971da4c8d2500420cfb6805b4c800 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 15 Feb 2024 13:43:23 +0100 Subject: [PATCH 076/483] refactor(org): remove individual cards screenshots Signed-off-by: Camila Belo --- plugins/org/OrgGroupProfileEntityCard.png | Bin 25091 -> 0 bytes plugins/org/OrgMembersListCard.png | Bin 48346 -> 0 bytes plugins/org/OrgOwnershipCard.png | Bin 118385 -> 0 bytes plugins/org/OrgUserProfileEntityCard.png | Bin 16209 -> 0 bytes plugins/org/README-alpha.md | 24 +++++++++++----------- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 plugins/org/OrgGroupProfileEntityCard.png delete mode 100644 plugins/org/OrgMembersListCard.png delete mode 100644 plugins/org/OrgOwnershipCard.png delete mode 100644 plugins/org/OrgUserProfileEntityCard.png diff --git a/plugins/org/OrgGroupProfileEntityCard.png b/plugins/org/OrgGroupProfileEntityCard.png deleted file mode 100644 index c012b54d04bdceb776edcc8579e4654a9d410fa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25091 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

yJPQ03ySfc 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 diff --git a/plugins/org/OrgMembersListCard.png b/plugins/org/OrgMembersListCard.png deleted file mode 100644 index 2a4d2b073127401bf57f7d0b028972d5a6ebab11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48346 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 diff --git a/plugins/org/OrgOwnershipCard.png b/plugins/org/OrgOwnershipCard.png deleted file mode 100644 index f9358a65d1b2de5b9afb532a50e8f8785f5e8c5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118385 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�?^v)$kUsg>&jaiG-^+hZ|+RyyY)F~^8 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⪻@(g?`0K{%>k1FIQlj5!jT*F~L|nYN8Ds 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 IE&G@M2jiSEuK)l5 diff --git a/plugins/org/OrgUserProfileEntityCard.png b/plugins/org/OrgUserProfileEntityCard.png deleted file mode 100644 index d163243ef1a25ef2759ebde889009cfebc6ae6b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16209 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 A-~a#s diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md index 35d4342070..fb9fe9d181 100644 --- a/plugins/org/README-alpha.md +++ b/plugins/org/README-alpha.md @@ -98,9 +98,9 @@ See a complete cards list below: This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view, edit, or update groups metadata, such as team avatar, name, email, parent, and child groups. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | --------------- | ------------------------------- | ----------------------------------------------------------------------------------------- | -| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | --------------- | ------------------------------- | +| `entity-card` | `org` | `group-profile` | `entity-card:org/group-profile` | ##### Disable @@ -173,9 +173,9 @@ For more information about where to place extension overrides, see the official An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays the names and emails of group members. By clicking the member's name, you'll be directed to the user's catalog page, and the email opens your default email program. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------- | -| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | -------------- | ------------------------------ | +| `entity-card` | `org` | `members-list` | `entity-card:org/members-list` | ##### Disable @@ -248,9 +248,9 @@ For more information about where to place extension overrides, see the official An [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension that displays direct or aggregated group or user ownership relationships. Each entity listed in the card links to its respective entity page in the catalog. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | ----------- | --------------------------- | -------------------------------------------------------------------------------- | -| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | ----------- | --------------------------- | +| `entity-card` | `org` | `ownership` | `entity-card:org/ownership` | ##### Disable @@ -323,9 +323,9 @@ For more information about where to place extension overrides, see the official This [entity card](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md) extension allows you to view user metadata including avatar, name, email, and team. Clicking on the email link will open your default email program while clicking on the team link will direct you to the team page in the catalog plugin. -| Kind | Namespace | Name | Id | Example | -| ------------- | --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------- | -| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | Entity Group Profile Card | +| Kind | Namespace | Name | Id | +| ------------- | --------- | -------------- | ------------------------------ | +| `entity-card` | `org` | `user-profile` | `entity-card:org/user-profile` | ##### Disable From 786c9c498f8722bd8d67e8857c5b2f9b5daaefb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:29:25 +0000 Subject: [PATCH 077/483] fix(deps): update dependency luxon to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-58582bb.md | 6 ++++++ plugins/linguist-backend/package.json | 2 +- plugins/linguist/package.json | 2 +- yarn.lock | 11 ++--------- 4 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 .changeset/renovate-58582bb.md diff --git a/.changeset/renovate-58582bb.md b/.changeset/renovate-58582bb.md new file mode 100644 index 0000000000..b1a58017f4 --- /dev/null +++ b/.changeset/renovate-58582bb.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-linguist': patch +--- + +Updated dependency `luxon` to `^3.0.0`. diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 2cc278ba44..3ff3c36f6e 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -45,7 +45,7 @@ "fs-extra": "^11.0.0", "knex": "^3.0.0", "linguist-js": "^2.5.3", - "luxon": "^2.0.2", + "luxon": "^3.0.0", "node-fetch": "^2.6.7", "uuid": "^8.3.2", "winston": "^3.2.1", diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 70885f0cbf..5b60c5217d 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -53,7 +53,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/lab": "4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "luxon": "^2.0.2", + "luxon": "^3.0.0", "react-use": "^17.2.4", "slugify": "^1.6.4" }, diff --git a/yarn.lock b/yarn.lock index 4b092ff735..174087abea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7603,7 +7603,7 @@ __metadata: js-yaml: ^4.1.0 knex: ^3.0.0 linguist-js: ^2.5.3 - luxon: ^2.0.2 + luxon: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 uuid: ^8.3.2 @@ -7640,7 +7640,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - luxon: ^2.0.2 + luxon: ^3.0.0 react-use: ^17.2.4 slugify: ^1.6.4 peerDependencies: @@ -34145,13 +34145,6 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^2.0.2": - version: 2.5.2 - resolution: "luxon@npm:2.5.2" - checksum: d8b671ffd2ff0b438af862ac11082a81b3aedd9f8b6a4ca636f944224f8dd381125cd4d274ca0a8aedcaf2cfca0fc2ca96fe4cc1b16a28b7af701afb95c0bff8 - languageName: node - linkType: hard - "luxon@npm:^3.0.0, luxon@npm:^3.3.0, luxon@npm:^3.4.3, luxon@npm:~3.4.0": version: 3.4.4 resolution: "luxon@npm:3.4.4" From f5e04e39d2e7da549309a21f008cc9f01c0b1e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 12:59:21 +0100 Subject: [PATCH 078/483] move away from deprecated types, import from auth-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/smart-frogs-help.md | 5 ++ plugins/auth-backend/api-report.md | 76 +++++++++---------- .../src/identity/StaticTokenIssuer.ts | 4 +- .../auth-backend/src/identity/TokenFactory.ts | 5 +- plugins/auth-backend/src/identity/types.ts | 2 +- .../src/lib/oauth/OAuthAdapter.test.ts | 4 +- .../src/lib/oauth/OAuthAdapter.ts | 10 +-- plugins/auth-backend/src/lib/oauth/helpers.ts | 4 +- plugins/auth-backend/src/lib/oauth/types.ts | 3 +- .../lib/passport/PassportStrategyHelper.ts | 4 +- .../resolvers/CatalogAuthResolverContext.ts | 9 ++- .../src/providers/atlassian/provider.ts | 7 +- .../src/providers/auth0/provider.ts | 11 ++- .../src/providers/aws-alb/provider.ts | 7 +- .../providers/azure-easyauth/provider.test.ts | 3 +- .../src/providers/azure-easyauth/provider.ts | 16 ++-- .../src/providers/bitbucket/provider.test.ts | 2 +- .../src/providers/bitbucket/provider.ts | 7 +- .../bitbucketServer/provider.test.ts | 2 +- .../src/providers/bitbucketServer/provider.ts | 11 ++- .../cloudflare-access/provider.test.ts | 2 +- .../providers/cloudflare-access/provider.ts | 17 +++-- .../createAuthProviderIntegration.ts | 5 +- .../src/providers/gcp-iap/provider.ts | 7 +- .../src/providers/gitlab/provider.ts | 7 +- .../src/providers/google/provider.ts | 3 +- .../src/providers/microsoft/provider.ts | 3 +- .../src/providers/oauth2-proxy/provider.ts | 7 +- .../src/providers/oauth2/provider.ts | 7 +- .../src/providers/oidc/provider.ts | 3 +- .../src/providers/okta/provider.ts | 7 +- .../src/providers/onelogin/provider.ts | 11 ++- .../auth-backend/src/providers/providers.ts | 2 +- .../auth-backend/src/providers/resolvers.ts | 2 +- .../src/providers/saml/provider.ts | 16 ++-- plugins/auth-backend/src/providers/types.ts | 4 +- plugins/auth-backend/src/service/router.ts | 6 +- 37 files changed, 167 insertions(+), 134 deletions(-) create mode 100644 .changeset/smart-frogs-help.md diff --git a/.changeset/smart-frogs-help.md b/.changeset/smart-frogs-help.md new file mode 100644 index 0000000000..9c65c267bd --- /dev/null +++ b/.changeset/smart-frogs-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to no longer use deprecated types diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 40e5facbbd..8d81751b39 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -42,12 +42,12 @@ import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-au // @public @deprecated export type AuthHandler = ( input: TAuthResult, - context: AuthResolverContext, + context: AuthResolverContext_2, ) => Promise; // @public @deprecated export type AuthHandlerResult = { - profile: ProfileInfo; + profile: ProfileInfo_2; }; // @public @@ -168,13 +168,13 @@ export type CookieConfigurer = CookieConfigurer_2; export function createAuthProviderIntegration< TCreateOptions extends unknown[], TResolvers extends { - [name in string]: (...args: any[]) => SignInResolver; + [name in string]: (...args: any[]) => SignInResolver_2; }, >(config: { - create: (...args: TCreateOptions) => AuthProviderFactory; + create: (...args: TCreateOptions) => AuthProviderFactory_2; resolvers?: TResolvers; }): Readonly<{ - create: (...args: TCreateOptions) => AuthProviderFactory; + create: (...args: TCreateOptions) => AuthProviderFactory_2; resolvers: Readonly; }>; @@ -186,7 +186,7 @@ export function createRouter(options: RouterOptions): Promise; // @public export const defaultAuthProviderFactories: { - [providerId: string]: AuthProviderFactory; + [providerId: string]: AuthProviderFactory_2; }; // @public (undocumented) @@ -226,13 +226,13 @@ export type GithubOAuthResult = { export type OAuth2ProxyResult = OAuth2ProxyResult_2; // @public @deprecated (undocumented) -export class OAuthAdapter implements AuthProviderRouteHandlers { +export class OAuthAdapter implements AuthProviderRouteHandlers_2 { constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions); // (undocumented) frameHandler(req: express.Request, res: express.Response): Promise; // (undocumented) static fromConfig( - config: AuthProviderConfig, + config: AuthProviderConfig_2, handlers: OAuthHandlers, options: Pick< OAuthAdapterOptions, @@ -253,7 +253,7 @@ export type OAuthAdapterOptions = { persistScopes?: boolean; appOrigin: string; baseUrl: string; - cookieConfigurer: CookieConfigurer; + cookieConfigurer: CookieConfigurer_2; isOriginAllowed: (origin: string) => boolean; callbackUrl: string; }; @@ -303,7 +303,7 @@ export type OAuthRefreshRequest = express.Request<{}> & { // @public @deprecated (undocumented) export type OAuthResponse = { - profile: ProfileInfo; + profile: ProfileInfo_2; providerInfo: OAuthProviderInfo; backstageIdentity?: BackstageSignInResult; }; @@ -354,7 +354,7 @@ export type ProfileInfo = ProfileInfo_2; // @public (undocumented) export type ProviderFactories = { - [s: string]: AuthProviderFactory; + [s: string]: AuthProviderFactory_2; }; // @public @@ -366,7 +366,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -381,7 +381,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -395,7 +395,7 @@ export const providers: Readonly<{ | { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; } | undefined, @@ -409,15 +409,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - usernameMatchingUserEntityAnnotation(): SignInResolver; - userIdMatchingUserEntityAnnotation(): SignInResolver; + usernameMatchingUserEntityAnnotation(): SignInResolver_2; + userIdMatchingUserEntityAnnotation(): SignInResolver_2; }>; }>; bitbucketServer: Readonly<{ @@ -427,33 +427,33 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; cfAccess: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; cache?: CacheService | undefined; }) => AuthProviderFactory_2; resolvers: Readonly<{ - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; gcpIap: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; }) => AuthProviderFactory_2; resolvers: never; @@ -483,7 +483,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -498,7 +498,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -517,7 +517,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -536,7 +536,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -548,7 +548,7 @@ export const providers: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; }) => AuthProviderFactory_2; resolvers: never; @@ -560,15 +560,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; }>; }>; okta: Readonly<{ @@ -578,16 +578,16 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; + emailMatchingUserEntityAnnotation(): SignInResolver_2; }>; }>; onelogin: Readonly<{ @@ -597,7 +597,7 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } @@ -612,14 +612,14 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; } | undefined, ) => AuthProviderFactory_2; resolvers: Readonly<{ - nameIdMatchingUserEntityName(): SignInResolver; + nameIdMatchingUserEntityName(): SignInResolver_2; }>; }>; easyAuth: Readonly<{ @@ -628,7 +628,7 @@ export const providers: Readonly<{ | { authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver_2; }; } | undefined, diff --git a/plugins/auth-backend/src/identity/StaticTokenIssuer.ts b/plugins/auth-backend/src/identity/StaticTokenIssuer.ts index 41dc96e71c..17fbe1b180 100644 --- a/plugins/auth-backend/src/identity/StaticTokenIssuer.ts +++ b/plugins/auth-backend/src/identity/StaticTokenIssuer.ts @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AnyJWK, TokenIssuer, TokenParams } from './types'; + +import { AnyJWK, TokenIssuer } from './types'; import { SignJWT, importJWK, JWK } from 'jose'; import { parseEntityRef } from '@backstage/catalog-model'; import { AuthenticationError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; import { StaticKeyStore } from './StaticKeyStore'; +import { TokenParams } from '@backstage/plugin-auth-node'; const MS_IN_S = 1000; diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index f778077267..4c2e7bac7a 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { parseEntityRef } from '@backstage/catalog-model'; import { AuthenticationError } from '@backstage/errors'; import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; import { LoggerService } from '@backstage/backend-plugin-api'; - -import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types'; +import { TokenParams } from '@backstage/plugin-auth-node'; +import { AnyJWK, KeyStore, TokenIssuer } from './types'; const MS_IN_S = 1000; const MAX_TOKEN_LENGTH = 32768; // At 64 bytes per entity ref this still leaves room for about 500 entities diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index fcfc0345cc..059b9fca84 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -37,7 +37,7 @@ export type TokenIssuer = { /** * Issues a new ID Token */ - issueToken(params: TokenParams): Promise; + issueToken(params: _TokenParams): Promise; /** * List all public keys that are currently being used to sign tokens, or have been used diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 1b6653d97e..48163e6e82 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -17,8 +17,8 @@ import express from 'express'; import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthHandlers, OAuthLogoutRequest, OAuthState } from './types'; -import { CookieConfigurer } from '../../providers/types'; +import { OAuthHandlers, OAuthLogoutRequest } from './types'; +import { CookieConfigurer, OAuthState } from '@backstage/plugin-auth-node'; const mockResponseData = { providerInfo: { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 9b4afb4b2b..b5c3642024 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -18,14 +18,13 @@ import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { URL } from 'url'; import { + AuthProviderConfig, + AuthProviderRouteHandlers, BackstageIdentityResponse, BackstageSignInResult, -} from '@backstage/plugin-auth-node'; -import { - AuthProviderRouteHandlers, - AuthProviderConfig, CookieConfigurer, -} from '../../providers/types'; + OAuthState, +} from '@backstage/plugin-auth-node'; import { AuthenticationError, InputError, @@ -42,7 +41,6 @@ import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest, - OAuthState, OAuthLogoutRequest, } from './types'; import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 5e67b072e3..fef6dd04ae 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -15,9 +15,9 @@ */ import express from 'express'; -import { OAuthState } from './types'; -import { CookieConfigurer } from '../../providers/types'; import { + CookieConfigurer, + OAuthState, decodeOAuthState, encodeOAuthState, } from '@backstage/plugin-auth-node'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index b7205c9b85..76689abcdc 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -18,9 +18,10 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; import { BackstageSignInResult, + ProfileInfo, OAuthState as _OAuthState, } from '@backstage/plugin-auth-node'; -import { OAuthStartResponse, ProfileInfo } from '../../providers/types'; +import { OAuthStartResponse } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 44feb916c5..88d3dbfa04 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -18,9 +18,9 @@ import express from 'express'; import passport from 'passport'; import { decodeJwt } from 'jose'; import { InternalOAuthError } from 'passport-oauth2'; - +import { ProfileInfo } from '@backstage/plugin-auth-node'; import { PassportProfile } from './types'; -import { ProfileInfo, OAuthStartResponse } from '../../providers/types'; +import { OAuthStartResponse } from '../../providers/types'; export type PassportDoneCallback = ( err?: Error, diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index 7d22526193..a7e02c1c31 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -25,10 +25,13 @@ import { } from '@backstage/catalog-model'; import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { TokenIssuer, TokenParams } from '../../identity/types'; -import { AuthResolverContext } from '../../providers'; -import { AuthResolverCatalogUserQuery } from '../../providers/types'; +import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../catalog'; +import { + AuthResolverCatalogUserQuery, + AuthResolverContext, + TokenParams, +} from '@backstage/plugin-auth-node'; /** * Uses the default ownership resolution logic to return an array diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index a142d1de90..0cd95e19d7 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 95e83d2cce..c399eb08c4 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -36,14 +36,13 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - AuthResolverContext, -} from '../types'; +import { OAuthStartResponse, AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { StateStore } from 'passport-oauth2'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 3883bbc88c..c09f307e96 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -18,8 +18,11 @@ import { AwsAlbResult, awsAlbAuthenticator, } from '@backstage/plugin-auth-backend-module-aws-alb-provider'; -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; -import { AuthHandler, SignInResolver } from '../types'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; /** diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts index 1b7c51263c..f6a418b633 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthHandler, AuthResolverContext } from '../types'; +import { AuthHandler } from '../types'; import { makeProfileInfo } from '../../lib/passport'; import { easyAuth, @@ -26,6 +26,7 @@ import { import { Request, Response } from 'express'; import { SignJWT, JWTPayload, errors as JoseErrors } from 'jose'; import { randomBytes } from 'crypto'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const jwtSecret = randomBytes(48); diff --git a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts index ca8cacf892..6f6fe72307 100644 --- a/plugins/auth-backend/src/providers/azure-easyauth/provider.ts +++ b/plugins/auth-backend/src/providers/azure-easyauth/provider.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - AuthResponse, - SignInResolver, -} from '../types'; +import { AuthHandler } from '../types'; import { Request, Response } from 'express'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; @@ -28,6 +22,12 @@ import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityRes import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Profile } from 'passport'; import { decodeJwt } from 'jose'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; export const ID_TOKEN_HEADER = 'x-ms-token-aad-id-token'; export const ACCESS_TOKEN_HEADER = 'x-ms-token-aad-access-token'; @@ -44,7 +44,7 @@ export type EasyAuthResult = { accessToken?: string; }; -export type EasyAuthResponse = AuthResponse<{}>; +export type EasyAuthResponse = ClientAuthResponse<{}>; export class EasyAuthAuthProvider implements AuthProviderRouteHandlers { private readonly resolverContext: AuthResolverContext; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts index 503eccffbd..66a8f6e396 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -16,7 +16,7 @@ import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { AuthResolverContext } from '../types'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const mockFrameHandler = jest.spyOn( helpers, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index cfa30e9a73..4a7f3770a4 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -37,12 +37,11 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthHandler, OAuthStartResponse } from '../types'; import { - AuthHandler, - OAuthStartResponse, - SignInResolver, AuthResolverContext, -} from '../types'; + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index f31d653b99..187c3b09a5 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -16,7 +16,6 @@ import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport'; -import { AuthResolverContext } from '../types'; import { bitbucketServer, BitbucketServerAuthProvider, @@ -25,6 +24,7 @@ import { import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; jest.mock('../../lib/passport/PassportStrategyHelper', () => { return { diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts index 220f6db5cd..d66f7f33c0 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -32,17 +32,16 @@ import { executeRefreshTokenStrategy, makeProfileInfo, } from '../../lib/passport'; -import { - AuthHandler, - AuthResolverContext, - OAuthStartResponse, - SignInResolver, -} from '../types'; +import { AuthHandler, OAuthStartResponse } from '../types'; import express from 'express'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { Profile as PassportProfile } from 'passport'; import { commonByEmailResolver } from '../resolvers'; import fetch from 'node-fetch'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts index 1abca4bdf4..95e4b0901b 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts @@ -21,8 +21,8 @@ import { CF_AUTH_IDENTITY, CloudflareAccessAuthProvider, } from './provider'; -import { AuthResolverContext } from '../types'; import fetch from 'node-fetch'; +import { AuthResolverContext } from '@backstage/plugin-auth-node'; const jwtMock = jwtVerify as jest.Mocked; const mockJwt = diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts index 6313276508..fe271f9058 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts @@ -13,13 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - AuthResponse, - SignInResolver, -} from '../types'; + +import { AuthHandler } from '../types'; import fetch, { Headers } from 'node-fetch'; import express from 'express'; import * as _ from 'lodash'; @@ -33,6 +28,12 @@ import { CacheClient } from '@backstage/backend-common'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { commonByEmailResolver } from '../resolvers'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; // JWT Web Token definitions are in the URL below // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/ @@ -174,7 +175,7 @@ export type CloudflareAccessProviderInfo = { }; export type CloudflareAccessResponse = - AuthResponse; + ClientAuthResponse; export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers { private readonly teamName: string; diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts index 9143d63143..9846bb8bf9 100644 --- a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { AuthProviderFactory, SignInResolver } from './types'; +import { + AuthProviderFactory, + SignInResolver, +} from '@backstage/plugin-auth-node'; /** * Creates a standardized representation of an integration with a third-party diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index db4d9195b2..e0c5b62d47 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -15,9 +15,12 @@ */ import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { GcpIapResult } from './types'; /** diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 2551d334ec..899338adce 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d467977094..b5e6e7127c 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -19,6 +19,7 @@ import { googleSignInResolvers, } from '@backstage/plugin-auth-backend-module-google-provider'; import { + SignInResolver, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; @@ -29,7 +30,7 @@ import { } from '../../lib/legacy'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; /** * Auth provider integration for Google auth diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b004cb9651..f6a3a83bcb 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { SignInResolver, AuthHandler } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { + SignInResolver, commonSignInResolvers, createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 5d75167e84..4200355c1a 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; -import { AuthHandler, SignInResolver } from '../types'; +import { + SignInResolver, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { type OAuth2ProxyResult, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index de6e7b1cfa..3a00de1f95 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -15,13 +15,16 @@ */ import { OAuthResult } from '../../lib/oauth'; -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, } from '../../lib/legacy'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { oauth2Authenticator } from '@backstage/plugin-auth-backend-module-oauth2-provider'; /** diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9bc78c48d2..40e837b0b0 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { createOAuthProviderFactory, @@ -22,6 +22,7 @@ import { BackstageSignInResult, OAuthAuthenticatorResult, SignInInfo, + SignInResolver, } from '@backstage/plugin-auth-node'; import { oidcAuthenticator, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 669914e7fc..463afc2bf4 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,11 +14,14 @@ * limitations under the License. */ -import { AuthHandler, SignInResolver } from '../types'; +import { AuthHandler } from '../types'; import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + SignInResolver, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { adaptLegacyOAuthHandler, adaptLegacyOAuthSignInResolver, diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index ac636f92cc..c5ba57f090 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -36,13 +36,12 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - AuthResolverContext, -} from '../types'; +import { OAuthStartResponse, AuthHandler } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + AuthResolverContext, + SignInResolver, +} from '@backstage/plugin-auth-node'; type PrivateInfo = { refreshToken: string; diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index 36a24f4f6c..76ac51f662 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -30,9 +30,9 @@ import { oidc } from './oidc'; import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; -import { AuthProviderFactory } from './types'; import { bitbucketServer } from './bitbucketServer'; import { easyAuth } from './azure-easyauth'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; /** * All built-in auth provider integrations. diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts index 129c29c5e4..54c78ff182 100644 --- a/plugins/auth-backend/src/providers/resolvers.ts +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SignInResolver } from './types'; +import { SignInResolver } from '@backstage/plugin-auth-node'; /** * A common sign-in resolver that looks up the user using the local part of diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 0da55ed3e4..d922034e2f 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -25,17 +25,17 @@ import { executeFrameHandlerStrategy, executeRedirectStrategy, } from '../../lib/passport'; -import { - AuthProviderRouteHandlers, - AuthHandler, - SignInResolver, - AuthResponse, - AuthResolverContext, -} from '../types'; +import { AuthHandler } from '../types'; import { postMessageResponse } from '../../lib/flow'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthenticationError, isError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + SignInResolver, +} from '@backstage/plugin-auth-node'; /** @public */ export type SamlAuthResult = { @@ -93,7 +93,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { const { profile } = await this.authHandler(result, this.resolverContext); - const response: AuthResponse<{}> = { + const response: ClientAuthResponse<{}> = { profile, providerInfo: {}, }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 354387153c..40c693506e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -110,7 +110,7 @@ export type SignInResolver = _SignInResolver; * @public * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ -export type AuthHandlerResult = { profile: ProfileInfo }; +export type AuthHandlerResult = { profile: _ProfileInfo }; /** * The AuthHandler function is called every time the user authenticates using @@ -128,7 +128,7 @@ export type AuthHandlerResult = { profile: ProfileInfo }; */ export type AuthHandler = ( input: TAuthResult, - context: AuthResolverContext, + context: _AuthResolverContext, ) => Promise; /** diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 44861207ed..a8876a7559 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -18,10 +18,7 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { - defaultAuthProviderFactories, - AuthProviderFactory, -} from '../providers'; +import { defaultAuthProviderFactories } from '../providers'; import { PluginDatabaseManager, PluginEndpointDiscovery, @@ -41,6 +38,7 @@ import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; /** @public */ export type ProviderFactories = { [s: string]: AuthProviderFactory }; From ff40ada6ba27e6ea846cad9212a10e8c2c7f4f50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:20:10 +0000 Subject: [PATCH 079/483] fix(deps): update dependency mysql2 to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-755938a.md | 6 +++ packages/backend-common/package.json | 4 +- packages/backend-test-utils/package.json | 2 +- packages/backend/package.json | 2 +- packages/e2e-test/package.json | 2 +- yarn.lock | 62 ++++++++++++------------ 6 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 .changeset/renovate-755938a.md diff --git a/.changeset/renovate-755938a.md b/.changeset/renovate-755938a.md new file mode 100644 index 0000000000..1182d57c16 --- /dev/null +++ b/.changeset/renovate-755938a.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-test-utils': patch +--- + +Updated dependency `mysql2` to `^3.0.0`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index ba39662e39..223bdc04ee 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -94,7 +94,7 @@ "logform": "^2.3.2", "luxon": "^3.0.0", "minimatch": "^5.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "node-fetch": "^2.6.7", "p-limit": "^3.1.0", "pg": "^8.11.3", @@ -133,7 +133,7 @@ "better-sqlite3": "^9.0.0", "http-errors": "^2.0.0", "msw": "^1.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "supertest": "^6.1.3" }, "files": [ diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 7d12ad8030..1516250232 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -54,7 +54,7 @@ "fs-extra": "^11.0.0", "knex": "^3.0.0", "msw": "^1.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pg": "^8.11.3", "testcontainers": "^8.1.2", "textextensions": "^5.16.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index d7039887e2..0772fbaef4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -93,7 +93,7 @@ "express-prom-bundle": "^7.0.0", "express-promise-router": "^4.1.0", "luxon": "^3.0.0", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pg": "^8.11.3", "pg-connection-string": "^2.3.0", "prom-client": "^15.0.0", diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 6848a0dd54..db5140f3ca 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -35,7 +35,7 @@ "cross-fetch": "^4.0.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.3", - "mysql2": "^2.2.5", + "mysql2": "^3.0.0", "pgtools": "^1.0.0", "tree-kill": "^1.2.2" }, diff --git a/yarn.lock b/yarn.lock index 3394871837..3b511bebd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3329,7 +3329,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^5.0.0 msw: ^1.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 pg: ^8.11.3 @@ -3483,7 +3483,7 @@ __metadata: fs-extra: ^11.0.0 knex: ^3.0.0 msw: ^1.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 pg: ^8.11.3 supertest: ^6.1.3 testcontainers: ^8.1.2 @@ -25436,7 +25436,7 @@ __metadata: languageName: node linkType: hard -"denque@npm:^2.0.1, denque@npm:^2.1.0": +"denque@npm:^2.1.0": version: 2.1.0 resolution: "denque@npm:2.1.0" checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 @@ -25990,7 +25990,7 @@ __metadata: cross-fetch: ^4.0.0 fs-extra: ^11.2.0 handlebars: ^4.7.3 - mysql2: ^2.2.5 + mysql2: ^3.0.0 nodemon: ^3.0.1 pgtools: ^1.0.0 tree-kill: ^1.2.2 @@ -27654,7 +27654,7 @@ __metadata: express-prom-bundle: ^7.0.0 express-promise-router: ^4.1.0 luxon: ^3.0.0 - mysql2: ^2.2.5 + mysql2: ^3.0.0 pg: ^8.11.3 pg-connection-string: ^2.3.0 prom-client: ^15.0.0 @@ -34008,17 +34008,10 @@ __metadata: languageName: node linkType: hard -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - -"long@npm:^5.0.0": - version: 5.2.0 - resolution: "long@npm:5.2.0" - checksum: 37aa4e67b9c3eebc6d9d675adcc9d06f06059ca268922a71273de389746bf07f0ff282f9e604d17fdf84c4149099b44e936ea2b621a6c4759a216621afa97efd +"long@npm:^5.0.0, long@npm:^5.2.1": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897 languageName: node linkType: hard @@ -34080,7 +34073,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^4.0.1, lru-cache@npm:^4.1.3": +"lru-cache@npm:^4.0.1": version: 4.1.5 resolution: "lru-cache@npm:4.1.5" dependencies: @@ -34108,13 +34101,20 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.14.0, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.14.0, lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 languageName: node linkType: hard +"lru-cache@npm:^8.0.0": + version: 8.0.5 + resolution: "lru-cache@npm:8.0.5" + checksum: 87d72196d8f46e8299c4ab576ed2ec8a07e3cbef517dc9874399c0b2470bd9bf62aacec3b67f84ed6d74aaa1ef31636d048edf996f76248fd17db72bfb631609 + languageName: node + linkType: hard + "lru-cache@npm:^9.0.0": version: 9.1.2 resolution: "lru-cache@npm:9.1.2" @@ -35780,19 +35780,19 @@ __metadata: languageName: node linkType: hard -"mysql2@npm:^2.2.5": - version: 2.3.3 - resolution: "mysql2@npm:2.3.3" +"mysql2@npm:^3.0.0": + version: 3.9.1 + resolution: "mysql2@npm:3.9.1" dependencies: - denque: ^2.0.1 + denque: ^2.1.0 generate-function: ^2.3.1 iconv-lite: ^0.6.3 - long: ^4.0.0 - lru-cache: ^6.0.0 - named-placeholders: ^1.1.2 + long: ^5.2.1 + lru-cache: ^8.0.0 + named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 45e479d0cbdb24ceb9d1846a1708ae2c33aa64f603f7899279b33560b1eec441f1b7a596075896f1305f701cfbc083bceb88bc72ba5d2f3656a3d6102611286a + checksum: 067353f8735d3e91654ecc01f562729c87f4fa141e870d524af06c5db98ac3341d91f3130357fead247833f41b1b93d1c6dd6e1f1b98687d28998bd9673b9ce7 languageName: node linkType: hard @@ -35807,12 +35807,12 @@ __metadata: languageName: node linkType: hard -"named-placeholders@npm:^1.1.2": - version: 1.1.2 - resolution: "named-placeholders@npm:1.1.2" +"named-placeholders@npm:^1.1.3": + version: 1.1.3 + resolution: "named-placeholders@npm:1.1.3" dependencies: - lru-cache: ^4.1.3 - checksum: c9317d1b479d6733b3baedfde209c6c866cf387c2d625837f93355fdb6a9055b1e8180b883fe00bcb20edb3ba4dd21128ec2f1ed8cb884385cef7698cbcadcc4 + lru-cache: ^7.14.1 + checksum: 7834adc91e92ae1b9c4413384e3ccd297de5168bb44017ff0536705ddc4db421723bd964607849265feb3f6ded390f84cf138e5925f22f7c13324f87a803dc73 languageName: node linkType: hard From e39eb80da9e7d66c3247503ee7cc2b5ff9c79a92 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 16 Feb 2024 13:26:34 +0000 Subject: [PATCH 080/483] better docs Signed-off-by: David Roberts --- plugins/azure-devops/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 6521f0e237..935e78c82c 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -70,9 +70,7 @@ dev.azure.com/project-repo: / dev.azure.com/build-definition: ``` -...and which README file belongs to each entity. - -Example: +Then to display the `README` file that belongs to each entity you would do this: ```yaml dev.azure.com/readme-path: //.md From fa7ea3f2f0810ae5b82b6829801208d2599f1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Feb 2024 14:33:10 +0100 Subject: [PATCH 081/483] break out the providers router into a separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/six-sloths-listen.md | 5 + plugins/auth-backend/src/identity/index.ts | 2 +- plugins/auth-backend/src/identity/router.ts | 17 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 1 + .../src/{service => providers}/router.test.ts | 0 plugins/auth-backend/src/providers/router.ts | 155 ++++++++++++++++++ plugins/auth-backend/src/service/index.ts | 17 ++ plugins/auth-backend/src/service/router.ts | 133 +++------------ 9 files changed, 208 insertions(+), 124 deletions(-) create mode 100644 .changeset/six-sloths-listen.md rename plugins/auth-backend/src/{service => providers}/router.test.ts (100%) create mode 100644 plugins/auth-backend/src/providers/router.ts create mode 100644 plugins/auth-backend/src/service/index.ts diff --git a/.changeset/six-sloths-listen.md b/.changeset/six-sloths-listen.md new file mode 100644 index 0000000000..47886e3f77 --- /dev/null +++ b/.changeset/six-sloths-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Internal refactor to break out how the router is constructed diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index a5e0dd4b80..0492836723 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { createOidcRouter } from './router'; +export { bindOidcRouter } from './router'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/identity/router.ts b/plugins/auth-backend/src/identity/router.ts index c9ae9e685a..c3c419e9ea 100644 --- a/plugins/auth-backend/src/identity/router.ts +++ b/plugins/auth-backend/src/identity/router.ts @@ -14,18 +14,21 @@ * limitations under the License. */ +import express from 'express'; import Router from 'express-promise-router'; import { TokenIssuer } from './types'; -export type Options = { - baseUrl: string; - tokenIssuer: TokenIssuer; -}; - -export function createOidcRouter(options: Options) { +export function bindOidcRouter( + targetRouter: express.Router, + options: { + baseUrl: string; + tokenIssuer: TokenIssuer; + }, +) { const { baseUrl, tokenIssuer } = options; const router = Router(); + targetRouter.use(router); const config = { issuer: baseUrl, @@ -68,6 +71,4 @@ export function createOidcRouter(options: Options) { router.get('/v1/userinfo', (_req, res) => { res.status(501).send('Not Implemented'); }); - - return router; } diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 3943387e15..6c3f866031 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export { authPlugin as default } from './authPlugin'; -export * from './service/router'; +export * from './service'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 8173a7fbc3..b9543c275c 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,6 +34,7 @@ export type { SamlAuthResult } from './saml'; export type { GcpIapResult, GcpIapTokenInfo } from './gcp-iap'; export { providers, defaultAuthProviderFactories } from './providers'; +export { createOriginFilter, type ProviderFactories } from './router'; export { createAuthProviderIntegration } from './createAuthProviderIntegration'; diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/providers/router.test.ts similarity index 100% rename from plugins/auth-backend/src/service/router.test.ts rename to plugins/auth-backend/src/providers/router.test.ts diff --git a/plugins/auth-backend/src/providers/router.ts b/plugins/auth-backend/src/providers/router.ts new file mode 100644 index 0000000000..9971466ad4 --- /dev/null +++ b/plugins/auth-backend/src/providers/router.ts @@ -0,0 +1,155 @@ +/* + * 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { NotFoundError, assertError } from '@backstage/errors'; +import { AuthProviderFactory } from '@backstage/plugin-auth-node'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Minimatch } from 'minimatch'; +import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext'; +import { TokenIssuer } from '../identity/types'; + +/** @public */ +export type ProviderFactories = { [s: string]: AuthProviderFactory }; + +export function bindProviderRouters( + targetRouter: express.Router, + options: { + providers: ProviderFactories; + appUrl: string; + baseUrl: string; + config: Config; + logger: LoggerService; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + catalogApi?: CatalogApi; + }, +) { + const { + providers, + appUrl, + baseUrl, + config, + logger, + discovery, + tokenManager, + tokenIssuer, + catalogApi, + } = options; + + const providersConfig = config.getOptionalConfig('auth.providers'); + + const isOriginAllowed = createOriginFilter(config); + + for (const [providerId, providerFactory] of Object.entries(providers)) { + if (providersConfig?.has(providerId)) { + logger.info(`Configuring auth provider: ${providerId}`); + try { + const provider = providerFactory({ + providerId, + appUrl, + baseUrl: baseUrl, + isOriginAllowed, + globalConfig: { + baseUrl: baseUrl, + appUrl, + isOriginAllowed, + }, + config: providersConfig.getConfig(providerId), + logger, + resolverContext: CatalogAuthResolverContext.create({ + logger, + catalogApi: + catalogApi ?? new CatalogClient({ discoveryApi: discovery }), + tokenIssuer, + tokenManager, + }), + }); + + const r = Router(); + + r.get('/start', provider.start.bind(provider)); + r.get('/handler/frame', provider.frameHandler.bind(provider)); + r.post('/handler/frame', provider.frameHandler.bind(provider)); + if (provider.logout) { + r.post('/logout', provider.logout.bind(provider)); + } + if (provider.refresh) { + r.get('/refresh', provider.refresh.bind(provider)); + r.post('/refresh', provider.refresh.bind(provider)); + } + + targetRouter.use(`/${providerId}`, r); + } catch (e) { + assertError(e); + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerId} auth provider, ${e.message}`, + ); + } + + logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); + + targetRouter.use(`/${providerId}`, () => { + // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. + throw new NotFoundError( + `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + + `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + + `Check the auth backend plugin logs when the backend starts to see more details.`, + ); + }); + } + } else { + targetRouter.use(`/${providerId}`, () => { + throw new NotFoundError( + `No auth provider registered for '${providerId}'`, + ); + }); + } + } +} + +/** @public */ +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { + const appUrl = config.getString('app.baseUrl'); + const { origin: appOrigin } = new URL(appUrl); + + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); + + const allowedOriginPatterns = + allowedOrigins?.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ) ?? []; + + return origin => { + if (origin === appOrigin) { + return true; + } + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} diff --git a/plugins/auth-backend/src/service/index.ts b/plugins/auth-backend/src/service/index.ts new file mode 100644 index 0000000000..d26055aa59 --- /dev/null +++ b/plugins/auth-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { createRouter, type RouterOptions } from './router'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index a8876a7559..459c347835 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -24,24 +24,19 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { assertError, NotFoundError } from '@backstage/errors'; -import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; +import { NotFoundError } from '@backstage/errors'; +import { CatalogApi } from '@backstage/catalog-client'; +import { bindOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; -import { Minimatch } from 'minimatch'; -import { CatalogAuthResolverContext } from '../lib/resolvers'; import { AuthDatabase } from '../database/AuthDatabase'; import { readBackstageTokenExpiration } from './readBackstageTokenExpiration'; import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { Config } from '@backstage/config'; -import { AuthProviderFactory } from '@backstage/plugin-auth-node'; - -/** @public */ -export type ProviderFactories = { [s: string]: AuthProviderFactory }; +import { ProviderFactories, bindProviderRouters } from '../providers/router'; /** @public */ export interface RouterOptions { @@ -65,10 +60,8 @@ export async function createRouter( config, discovery, database, - tokenManager, tokenFactoryAlgorithm, providerFactories = {}, - catalogApi, } = options; const router = Router(); @@ -103,6 +96,7 @@ export async function createRouter( config.getOptionalString('auth.identityTokenAlgorithm'), }); } + const secret = config.getOptionalString('auth.session.secret'); if (secret) { router.use(cookieParser(secret)); @@ -125,96 +119,31 @@ export async function createRouter( } else { router.use(cookieParser()); } + router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = options.disableDefaultProviderFactories + const providers = options.disableDefaultProviderFactories ? providerFactories : { ...defaultAuthProviderFactories, ...providerFactories, }; - const providersConfig = config.getOptionalConfig('auth.providers'); + bindProviderRouters(router, { + providers, + appUrl, + baseUrl: authUrl, + tokenIssuer, + ...options, + }); - const isOriginAllowed = createOriginFilter(config); - - for (const [providerId, providerFactory] of Object.entries( - allProviderFactories, - )) { - if (providersConfig?.has(providerId)) { - logger.info(`Configuring auth provider: ${providerId}`); - try { - const provider = providerFactory({ - providerId, - appUrl, - baseUrl: authUrl, - isOriginAllowed, - globalConfig: { - baseUrl: authUrl, - appUrl, - isOriginAllowed, - }, - config: providersConfig.getConfig(providerId), - logger, - resolverContext: CatalogAuthResolverContext.create({ - logger, - catalogApi: - catalogApi ?? new CatalogClient({ discoveryApi: discovery }), - tokenIssuer, - tokenManager, - }), - }); - - const r = Router(); - - r.get('/start', provider.start.bind(provider)); - r.get('/handler/frame', provider.frameHandler.bind(provider)); - r.post('/handler/frame', provider.frameHandler.bind(provider)); - if (provider.logout) { - r.post('/logout', provider.logout.bind(provider)); - } - if (provider.refresh) { - r.get('/refresh', provider.refresh.bind(provider)); - r.post('/refresh', provider.refresh.bind(provider)); - } - - router.use(`/${providerId}`, r); - } catch (e) { - assertError(e); - if (process.env.NODE_ENV !== 'development') { - throw new Error( - `Failed to initialize ${providerId} auth provider, ${e.message}`, - ); - } - - logger.warn(`Skipping ${providerId} auth provider, ${e.message}`); - - router.use(`/${providerId}`, () => { - // If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found. - throw new NotFoundError( - `Auth provider registered for '${providerId}' is misconfigured. This could mean the configs under ` + - `auth.providers.${providerId} are missing or the environment variables used are not defined. ` + - `Check the auth backend plugin logs when the backend starts to see more details.`, - ); - }); - } - } else { - router.use(`/${providerId}`, () => { - throw new NotFoundError( - `No auth provider registered for '${providerId}'`, - ); - }); - } - } - - router.use( - createOidcRouter({ - tokenIssuer, - baseUrl: authUrl, - }), - ); + bindOidcRouter(router, { + tokenIssuer, + baseUrl: authUrl, + }); + // Gives a more helpful error message than a plain 404 router.use('/:provider/', req => { const { provider } = req.params; throw new NotFoundError(`Unknown auth provider '${provider}'`); @@ -222,27 +151,3 @@ export async function createRouter( return router; } - -/** @public */ -export function createOriginFilter( - config: Config, -): (origin: string) => boolean { - const appUrl = config.getString('app.baseUrl'); - const { origin: appOrigin } = new URL(appUrl); - - const allowedOrigins = config.getOptionalStringArray( - 'auth.experimentalExtraAllowedOrigins', - ); - - const allowedOriginPatterns = - allowedOrigins?.map( - pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), - ) ?? []; - - return origin => { - if (origin === appOrigin) { - return true; - } - return allowedOriginPatterns.some(pattern => pattern.match(origin)); - }; -} From 4642cb7ac23211407877bcd780682ce2e9478e77 Mon Sep 17 00:00:00 2001 From: Andres Mauricio Gomez P Date: Fri, 16 Feb 2024 13:28:32 -0500 Subject: [PATCH 082/483] Added support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments Signed-off-by: Andres Mauricio Gomez P --- .changeset/two-planets-beam.md | 6 + plugins/kubernetes-common/api-report.md | 2 + plugins/kubernetes-common/src/types.ts | 1 + .../kubernetes-common/src/util/response.ts | 4 + .../src/__fixtures__/2-daemonsets.json | 581 ++++++++++++++++++ .../src/components/Cluster/Cluster.tsx | 6 + .../DaemonSetsAccordions.test.tsx | 33 + .../DaemonSetsAccordions.tsx | 147 +++++ .../DaemonSetsDrawer.test.tsx | 68 ++ .../DaemonSetsAccordions/DaemonSetsDrawer.tsx | 76 +++ .../components/DaemonSetsAccordions/index.ts | 16 + .../src/hooks/GroupedResponses.ts | 1 + 12 files changed, 941 insertions(+) create mode 100644 .changeset/two-planets-beam.md create mode 100644 plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx create mode 100644 plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts diff --git a/.changeset/two-planets-beam.md b/.changeset/two-planets-beam.md new file mode 100644 index 0000000000..d500d22853 --- /dev/null +++ b/.changeset/two-planets-beam.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-kubernetes-react': patch +--- + +Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index c1d399b196..2f46ef90e5 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -275,6 +275,8 @@ export interface GroupedResponses extends DeploymentResources { // (undocumented) customResources: any[]; // (undocumented) + daemonSets: V1DaemonSet[]; + // (undocumented) ingresses: V1Ingress[]; // (undocumented) jobs: V1Job[]; diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index cd316af089..c4bb5fd32f 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -294,4 +294,5 @@ export interface GroupedResponses extends DeploymentResources { cronJobs: V1CronJob[]; customResources: any[]; statefulsets: V1StatefulSet[]; + daemonSets: V1DaemonSet[]; } diff --git a/plugins/kubernetes-common/src/util/response.ts b/plugins/kubernetes-common/src/util/response.ts index a86d90f780..b62fa162d1 100644 --- a/plugins/kubernetes-common/src/util/response.ts +++ b/plugins/kubernetes-common/src/util/response.ts @@ -58,6 +58,9 @@ export const groupResponses = ( case 'statefulsets': prev.statefulsets.push(...next.resources); break; + case 'daemonsets': + prev.daemonSets.push(...next.resources); + break; default: } return prev; @@ -74,6 +77,7 @@ export const groupResponses = ( cronJobs: [], customResources: [], statefulsets: [], + daemonSets: [], } as GroupedResponses, ); }; diff --git a/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json new file mode 100644 index 0000000000..1c88b9d8a7 --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/2-daemonsets.json @@ -0,0 +1,581 @@ +{ + "daemonSets": [ + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"fluentd-elasticsearch\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-14T21:00:28Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch", + "namespace": "default", + "resourceVersion": "1769498", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "name": "fluentd-elasticsearch" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + }, + { + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "metadata": { + "annotations": { + "deprecated.daemonset.template.generation": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch2\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch2\"}},\"template\":{\"metadata\":{\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\",\"name\":\"fluentd-elasticsearch2\"}},\"spec\":{\"containers\":[{\"image\":\"quay.io/fluentd_elasticsearch/fluentd:v2.5.2\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}],\"terminationGracePeriodSeconds\":30}}}}\n" + }, + "creationTimestamp": "2024-02-16T18:11:26Z", + "generation": 1, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "k8s-app": "fluentd-logging" + }, + "name": "fluentd-elasticsearch2", + "namespace": "default", + "resourceVersion": "1981736", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + }, + "spec": { + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "name": "fluentd-elasticsearch2" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "name": "fluentd-elasticsearch2" + } + }, + "spec": { + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 0, + "maxUnavailable": 1 + }, + "type": "RollingUpdate" + } + }, + "status": { + "currentNumberScheduled": 1, + "desiredNumberScheduled": 1, + "numberAvailable": 1, + "numberMisscheduled": 0, + "numberReady": 1, + "observedGeneration": 1, + "updatedNumberScheduled": 1 + } + } + ], + "pods": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T16:35:06Z", + "generateName": "fluentd-elasticsearch-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "86978587c8", + "name": "fluentd-elasticsearch", + "pod-template-generation": "2" + }, + "name": "fluentd-elasticsearch-mmkpf", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch", + "uid": "2ba243f3-a733-4b63-9db9-c9b8ce303a23" + } + ], + "resourceVersion": "1970744", + "uid": "fc9f9070-1a3c-4e09-9cd6-8803a4ebfb0a" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-8cwbv", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-8cwbv", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:08Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T16:35:06Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://958e4ec14b6a3557724fabeaac9c8f8fe2ea797337a4397e700209f6e482e898", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T16:35:07Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.215", + "podIPs": [ + { + "ip": "10.244.0.215" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T16:35:06Z" + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2024-02-16T18:11:26Z", + "generateName": "fluentd-elasticsearch2-", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-revision-hash": "5979b599f6", + "name": "fluentd-elasticsearch2", + "pod-template-generation": "1" + }, + "name": "fluentd-elasticsearch2-7hwwg", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "DaemonSet", + "name": "fluentd-elasticsearch2", + "uid": "b923d035-e9a4-4a40-9472-f0f3468f30df" + } + ], + "resourceVersion": "1981735", + "uid": "1b829903-2c3a-453f-a8f2-b6603eec0bbe" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchFields": [ + { + "key": "metadata.name", + "operator": "In", + "values": ["ucp-control-plane"] + } + ] + } + ] + } + } + }, + "containers": [ + { + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imagePullPolicy": "IfNotPresent", + "name": "fluentd-elasticsearch", + "resources": { + "limits": { + "memory": "200Mi" + }, + "requests": { + "cpu": "100m", + "memory": "200Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-rpkbp", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "ucp-control-plane", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/disk-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/pid-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/unschedulable", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-rpkbp", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:28Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-02-16T18:11:27Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://3372cb5a300c87e5b1530b32fea7af35dce92470fc7c1cea47c714fac283feb9", + "image": "quay.io/fluentd_elasticsearch/fluentd:v2.5.2", + "imageID": "sha256:a5bf47027e067e0376708cae10750ea154b13356dfc0209610f5ea0bc7c16fe0", + "lastState": {}, + "name": "fluentd-elasticsearch", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-02-16T18:11:28Z" + } + } + } + ], + "hostIP": "172.19.0.3", + "phase": "Running", + "podIP": "10.244.0.88", + "podIPs": [ + { + "ip": "10.244.0.88" + } + ], + "qosClass": "Burstable", + "startTime": "2024-02-16T18:11:27Z" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index f2108ac1b7..d713b9a873 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -34,6 +34,7 @@ import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; +import { DaemonSetsAccordions } from '../DaemonSetsAccordions'; import { ClusterContext, GroupedResponsesContext, @@ -151,6 +152,11 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ) : undefined} + {groupedResponses.daemonSets.length > 0 ? ( + + + + ) : undefined} {groupedResponses.statefulsets.length > 0 ? ( diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx new file mode 100644 index 0000000000..9f38abf897 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2020 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 React from 'react'; +import { screen } from '@testing-library/react'; +import { DaemonSetsAccordions } from './DaemonSetsAccordions'; +import * as oneDaemonsFixture from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('DaemonSetsAccordions', () => { + it('should render two daemonset', async () => { + const wrapper = kubernetesProviders(oneDaemonsFixture); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('fluentd-elasticsearch')).toBeInTheDocument(); + expect(screen.getByText('fluentd-elasticsearch2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx new file mode 100644 index 0000000000..834a86e837 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsAccordions.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2020 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 React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Pod, V1DaemonSet } from '@kubernetes/client-node'; +import { PodsTable } from '../Pods'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { + GroupedResponsesContext, + PodNamesWithErrorsContext, +} from '../../hooks'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable'; + +type DaemonSetsAccordionsProps = { + children?: React.ReactNode; +}; + +type DaemonSetAccordionProps = { + daemonset: V1DaemonSet; + ownedPods: V1Pod[]; + children?: React.ReactNode; +}; + +type DaemonSetSummaryProps = { + daemonset: V1DaemonSet; + numberOfCurrentPods: number; + numberOfPodsWithErrors: number; + children?: React.ReactNode; +}; + +const DaemonSetSummary = ({ + daemonset, + numberOfCurrentPods, + numberOfPodsWithErrors, +}: DaemonSetSummaryProps) => { + return ( + + + + + + + {numberOfCurrentPods} pods + + + {numberOfPodsWithErrors > 0 ? ( + + {numberOfPodsWithErrors} pod + {numberOfPodsWithErrors > 1 ? 's' : ''} with errors + + ) : ( + No pods with errors + )} + + + + ); +}; + +const DaemonSetAccordion = ({ + daemonset, + ownedPods, +}: DaemonSetAccordionProps) => { + const podNamesWithErrors = useContext(PodNamesWithErrorsContext); + + const podsWithErrors = ownedPods.filter(p => + podNamesWithErrors.has(p.metadata?.name ?? ''), + ); + + return ( + + }> + + + + + + + ); +}; + +export const DaemonSetsAccordions = ({}: DaemonSetsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {groupedResponses.daemonSets.map((daemonset, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx new file mode 100644 index 0000000000..51e70a3b9b --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2020 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 React from 'react'; +import * as daemonsets from '../../__fixtures__/2-daemonsets.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { DaemonSetDrawer } from './DaemonSetsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('DaemonsetsDrawer', () => { + it('should render daemonsets drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + , + ); + expect(getAllByText('fluentd-elasticsearch')).toHaveLength(2); + expect(getAllByText('DaemonSet')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Update Strategy Type')).toBeInTheDocument(); + expect(getByText('RollingUpdate')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('???')).toBeInTheDocument(); + expect(getByText('Min Ready Seconds')).toBeInTheDocument(); + expect(getByText('Revision History Limit')).toBeInTheDocument(); + expect(getByText('Current Number Scheduled')).toBeInTheDocument(); + expect(getByText('Desired Number Scheduled')).toBeInTheDocument(); + expect(getByText('Number Available')).toBeInTheDocument(); + expect(getByText('Number Misscheduled')).toBeInTheDocument(); + expect(getByText('Number Ready')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); + + it('should render deployment drawer without namespace', async () => { + const daemonset = (daemonsets as any).daemonSets[0]; + const { queryByText } = await renderInTestApp( + + + , + , + ); + + expect(queryByText('namespace: default')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx new file mode 100644 index 0000000000..e0f64ed679 --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/DaemonSetsDrawer.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2020 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 React from 'react'; +import { V1DaemonSet } from '@kubernetes/client-node'; +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import { Typography, Grid, Chip } from '@material-ui/core'; + +export const DaemonSetDrawer = ({ + daemonset, + expanded, +}: { + daemonset: V1DaemonSet; + expanded?: boolean; +}) => { + const namespace = daemonset.metadata?.namespace; + return ( + { + return { + updateStrategyType: daemonsetObj.spec?.updateStrategy?.type ?? '???', + minReadySeconds: daemonsetObj.spec?.minReadySeconds ?? '???', + revisionHistoryLimit: + daemonsetObj.spec?.revisionHistoryLimit ?? '???', + currentNumberScheduled: + daemonsetObj.status?.currentNumberScheduled ?? '???', + desiredNumberScheduled: + daemonsetObj.status?.desiredNumberScheduled ?? '???', + numberAvailable: daemonsetObj.status?.numberAvailable ?? '???', + numberMisscheduled: daemonsetObj.status?.numberMisscheduled ?? '???', + numberReady: daemonsetObj.status?.numberReady ?? '???', + }; + }} + > + + + + {daemonset.metadata?.name ?? 'unknown object'} + + + + + DaemonSet + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts new file mode 100644 index 0000000000..ca25150cbd --- /dev/null +++ b/plugins/kubernetes-react/src/components/DaemonSetsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { DaemonSetsAccordions } from './DaemonSetsAccordions'; diff --git a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts index 7a81a74337..10cd3a8abe 100644 --- a/plugins/kubernetes-react/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes-react/src/hooks/GroupedResponses.ts @@ -25,6 +25,7 @@ export const GroupedResponsesContext = React.createContext({ pods: [], replicaSets: [], deployments: [], + daemonSets: [], services: [], configMaps: [], horizontalPodAutoscalers: [], From 44c817ae23fb3887c5647008ef381619505238a7 Mon Sep 17 00:00:00 2001 From: Aramis Date: Fri, 16 Feb 2024 18:19:45 -0500 Subject: [PATCH 083/483] chore: update my account for contributions Signed-off-by: Aramis --- OWNERS.md | 8 ++++---- scripts/check-docs-quality.js | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/OWNERS.md b/OWNERS.md index 1e92e68c40..f955a9de11 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -114,9 +114,9 @@ Team: @backstage/openapi-tooling-maintainers Scope: Tooling for frontend and backend schema-first OpenAPI development. -| Name | Organization | GitHub | Discord | -| -------------- | ------------ | --------------------------------------- | ------------- | -| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | +| Name | Organization | GitHub | Discord | +| -------------- | ------------ | ----------------------------------------------------- | ------------- | +| Aramis Sennyey | | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | ### Scaffolder @@ -144,7 +144,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` | | Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` | -| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | +| Aramis Sennyey | | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | | Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` | | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 119dd6bef4..9a9336b3e9 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -32,7 +32,11 @@ const IGNORED_WHEN_LISTING = [ /^docs[/\\]reference[/\\]/, ]; -const IGNORED_WHEN_EXPLICIT = [/^.*[/\\]knip-report\.md$/]; +const IGNORED_WHEN_EXPLICIT = [ + /^ADOPTERS\.md$/, + /^OWNERS\.md$/, + /^.*[/\\]knip-report\.md$/, +]; const rootDir = resolvePath(__dirname, '..'); @@ -120,7 +124,6 @@ async function main() { const relativePaths = absolutePaths .map(path => relativePath(rootDir, path)) .filter(path => !IGNORED_WHEN_EXPLICIT.some(pattern => pattern.test(path))); - const success = await runVale( relativePaths.length === 0 ? await listFiles() : relativePaths, ); From 562270b6dd883f4d961bd0e07724bb5366a2a495 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sat, 17 Feb 2024 15:08:35 +0100 Subject: [PATCH 084/483] wip Signed-off-by: bnechyporenko --- package.json | 1 + plugins/scaffolder-backend/api-report.md | 71 ++----------------- .../tasks/DatabaseTaskStore.test.ts | 8 ++- .../src/scaffolder/tasks/DatabaseTaskStore.ts | 31 ++------ .../tasks/NunjucksWorkflowRunner.test.ts | 30 ++++---- .../tasks/NunjucksWorkflowRunner.ts | 14 +++- .../src/scaffolder/tasks/StorageTaskBroker.ts | 16 +---- .../src/scaffolder/tasks/types.ts | 16 +---- plugins/scaffolder-node/api-report.md | 10 +-- plugins/scaffolder-node/src/tasks/types.ts | 7 +- yarn.lock | 1 + 11 files changed, 53 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index afc86f2460..e224d1e58a 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@backstage/repo-tools": "workspace:*", "@changesets/cli": "^2.14.0", "@octokit/rest": "^19.0.3", + "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^14.1.3", "@spotify/prettier-config": "^14.0.0", "@techdocs/cli": "workspace:*", diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4f005a0e28..f9a810ebea 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -360,17 +360,7 @@ export interface CurrentClaimedTask { createdBy?: string; secrets?: TaskSecrets_2; spec: TaskSpec; - state?: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + state?: JsonObject; taskId: string; } @@ -411,15 +401,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) getTaskState({ taskId }: { taskId: string }): Promise< | { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; + state: JsonObject; } | undefined >; @@ -445,22 +427,7 @@ export class DatabaseTaskStore implements TaskStore { ids: string[]; }>; // (undocumented) - saveTaskState(options: { - taskId: string; - state?: - | { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - } - | undefined; - }): Promise; + saveTaskState(options: { taskId: string; state?: JsonObject }): Promise; // (undocumented) shutdownTask(options: TaskStoreShutDownTaskOptions): Promise; } @@ -565,15 +532,7 @@ export class TaskManager implements TaskContext_2 { // (undocumented) getTaskState?(): Promise< | { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; + state?: JsonObject; } | undefined >; @@ -628,15 +587,7 @@ export interface TaskStore { // (undocumented) getTaskState?({ taskId }: { taskId: string }): Promise< | { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; + state: JsonObject; } | undefined >; @@ -663,17 +614,7 @@ export interface TaskStore { // (undocumented) saveTaskState?(options: { taskId: string; - state?: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; + state?: JsonObject; }): Promise; // (undocumented) shutdownTask?(options: TaskStoreShutDownTaskOptions): Promise; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index deb72fb7d2..8fda4b5090 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -209,9 +209,11 @@ describe('DatabaseTaskStore', () => { const state = await store.getTaskState({ taskId }); expect(state).toStrictEqual({ - 'repo.create': { - status: 'success', - value: { repoUrl: 'https://github.com/backstage/backstage.git' }, + state: { + 'repo.create': { + status: 'success', + value: { repoUrl: 'https://github.com/backstage/backstage.git' }, + }, }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 263fe3d435..6422e53ee9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/types'; +import { JsonObject } from '@backstage/types'; import { PluginDatabaseManager, resolvePackagePath, @@ -397,42 +397,19 @@ export class DatabaseTaskStore implements TaskStore { async getTaskState({ taskId }: { taskId: string }): Promise< | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; + state: JsonObject; } | undefined > { const [result] = await this.db('tasks') .where({ id: taskId }) .select('state'); - return result.state - ? (JSON.parse(result.state) as unknown as { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }) - : undefined; + return result.state ? { state: JSON.parse(result.state) } : undefined; } async saveTaskState(options: { taskId: string; - state?: - | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - } - | undefined; + state?: JsonObject; }): Promise { if (options.state) { const serializedState = JSON.stringify(options.state); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 8018812c07..eaf477f35f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -580,23 +580,27 @@ describe('NunjucksWorkflowRunner', () => { }), getTaskState: (): Promise< | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; + state: { + [key: string]: + | { status: 'failed'; reason: string } + | { + status: 'success'; + value: JsonValue; + }; + }; } | undefined > => { return Promise.resolve({ - ['v1.task.checkpoint.key1']: { - status: 'success', - value: 'initial', - }, - ['v1.task.checkpoint.key2']: { - status: 'failed', - reason: 'fatal error', + state: { + ['v1.task.checkpoint.key1']: { + status: 'success', + value: 'initial', + }, + ['v1.task.checkpoint.key2']: { + status: 'failed', + reason: 'fatal error', + }, }, }); }, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 1070972112..e3bf094009 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -79,6 +79,18 @@ type TemplateContext = { each?: JsonValue; }; +type TaskState = { + [key: string]: + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: JsonValue; + }; +}; + const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { return taskSpec.apiVersion === 'scaffolder.backstage.io/v1beta3'; }; @@ -356,7 +368,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { try { let prevValue: U | undefined; if (prevTaskState) { - const prevState = prevTaskState[key]; + const prevState = (prevTaskState.state as TaskState)?.[key]; if (prevState && prevState.status === 'success') { prevValue = prevState.value as U; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 2287279ea9..c437795778 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -93,12 +93,7 @@ export class TaskManager implements TaskContext { async getTaskState?(): Promise< | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; + state?: JsonObject; } | undefined > { @@ -186,14 +181,7 @@ export interface CurrentClaimedTask { /** * The state of checkpoints of the task. */ - state?: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + state?: JsonObject; /** * The creator of the task. */ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index d0bb48f4f2..c0854af391 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -196,26 +196,14 @@ export interface TaskStore { getTaskState?({ taskId }: { taskId: string }): Promise< | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; + state: JsonObject; } | undefined >; saveTaskState?(options: { taskId: string; - state?: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; + state?: JsonObject; }): Promise; listEvents( diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 7c024a0043..3f2e4e369f 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -351,15 +351,7 @@ export interface TaskContext { // (undocumented) getTaskState?(): Promise< | { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; + state?: JsonObject; } | undefined >; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 18383dbac3..0cca1a0ba2 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -128,12 +128,7 @@ export interface TaskContext { getTaskState?(): Promise< | { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; + state?: JsonObject; } | undefined >; diff --git a/yarn.lock b/yarn.lock index cd5f483ddc..65a4a0a508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41210,6 +41210,7 @@ __metadata: "@changesets/cli": ^2.14.0 "@manypkg/get-packages": ^1.1.3 "@octokit/rest": ^19.0.3 + "@playwright/test": ^1.32.3 "@spotify/eslint-plugin": ^14.1.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" From 2d40f79c44bd7433478cfbc9043d66b9b82c39b6 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 11:32:42 +0100 Subject: [PATCH 085/483] wip Signed-off-by: bnechyporenko --- plugins/scaffolder-node/api-report.md | 12 ------------ plugins/scaffolder-node/src/tasks/types.ts | 8 -------- 2 files changed, 20 deletions(-) diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 3f2e4e369f..76e22c0c5a 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -364,18 +364,6 @@ export interface TaskContext { // (undocumented) spec: TaskSpec; // (undocumented) - state?: { - [key: string]: - | { - status: 'failed'; - reason: string; - } - | { - status: 'success'; - value: JsonValue; - }; - }; - // (undocumented) updateCheckpoint?( options: | { diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index 0cca1a0ba2..e57668e346 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -110,14 +110,6 @@ export interface TaskContext { cancelSignal: AbortSignal; spec: TaskSpec; secrets?: TaskSecrets; - state?: { - [key: string]: - | { status: 'failed'; reason: string } - | { - status: 'success'; - value: JsonValue; - }; - }; createdBy?: string; done: boolean; isDryRun?: boolean; From 3f15fa93870cebbe8757445d0e262b30db58deb2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 18 Feb 2024 12:38:19 +0100 Subject: [PATCH 086/483] docs/backend-system: split core services into separate doc sections Signed-off-by: Patrik Oldsberg --- docs/backend-system/core-services/01-index.md | 680 +----------------- docs/backend-system/core-services/auth.md | 8 + docs/backend-system/core-services/cache.md | 38 + docs/backend-system/core-services/database.md | 47 ++ .../backend-system/core-services/discovery.md | 35 + .../backend-system/core-services/http-auth.md | 8 + .../core-services/http-router.md | 58 ++ docs/backend-system/core-services/identity.md | 73 ++ .../backend-system/core-services/lifecycle.md | 40 ++ docs/backend-system/core-services/logger.md | 77 ++ .../core-services/permissions.md | 53 ++ .../core-services/plugin-metadata.md | 8 + .../core-services/root-config.md | 62 ++ .../core-services/root-http-router.md | 76 ++ .../core-services/root-lifecycle.md | 65 ++ .../core-services/root-logger.md | 8 + .../backend-system/core-services/scheduler.md | 41 ++ .../core-services/token-manager.md | 8 + .../core-services/url-reader.md | 47 ++ .../backend-system/core-services/user-info.md | 8 + microsite/sidebars.json | 23 +- 21 files changed, 804 insertions(+), 659 deletions(-) create mode 100644 docs/backend-system/core-services/auth.md create mode 100644 docs/backend-system/core-services/cache.md create mode 100644 docs/backend-system/core-services/database.md create mode 100644 docs/backend-system/core-services/discovery.md create mode 100644 docs/backend-system/core-services/http-auth.md create mode 100644 docs/backend-system/core-services/http-router.md create mode 100644 docs/backend-system/core-services/identity.md create mode 100644 docs/backend-system/core-services/lifecycle.md create mode 100644 docs/backend-system/core-services/logger.md create mode 100644 docs/backend-system/core-services/permissions.md create mode 100644 docs/backend-system/core-services/plugin-metadata.md create mode 100644 docs/backend-system/core-services/root-config.md create mode 100644 docs/backend-system/core-services/root-http-router.md create mode 100644 docs/backend-system/core-services/root-lifecycle.md create mode 100644 docs/backend-system/core-services/root-logger.md create mode 100644 docs/backend-system/core-services/scheduler.md create mode 100644 docs/backend-system/core-services/token-manager.md create mode 100644 docs/backend-system/core-services/url-reader.md create mode 100644 docs/backend-system/core-services/user-info.md diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 9074998719..04d2e57ae8 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -8,666 +8,30 @@ description: Core backend service APIs The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, URL Readers, databases and more. -All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package. +All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package: ```ts import { coreServices } from '@backstage/backend-plugin-api'; ``` -## HTTP Router Service - -One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. - -### Using the service - -The following example shows how to register a HTTP router for the `example` plugin. -This single route will be available at the `/api/example/hello` path. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { http: coreServices.httpRouter }, - async init({ http }) { - const router = Router(); - router.get('/hello', (_req, res) => { - res.status(200).json({ hello: 'world' }); - }); - // Registers the router at the /api/example path - http.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `httpRouter` core service. - -- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { httpRouterServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - httpRouterServiceFactory({ - getPath: (pluginId: string) => `/plugins/${pluginId}`, - }), -); -``` - -## Root HTTP Router - -The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. - -### Using the service - -The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - rootHttpRouter: coreServices.rootHttpRouter, - }, - async init({ rootHttpRouter }) { - const router = Router(); - router.get('/health', (request, response) => { - response.send('OK'); - }); - - rootHttpRouter.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. - -- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. - -- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. - -You can configure the root HTTP Router service by passing the options to the `createBackend` function. - -```ts -import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - rootHttpRouterServiceFactory({ - configure: ({ app, middleware, routes, config, logger, lifecycle }) => { - // the built in middleware is provided through an option in the configure function - app.use(middleware.helmet()); - app.use(middleware.cors()); - app.use(middleware.compression()); - - // you can add you your own middleware in here - app.use(custom.logging()); - - // here the routes that are registered by other plugins - app.use(routes); - - // some other middleware that comes after the other routes - app.use(middleware.notFound()); - app.use(middleware.error()); - }, - }), -); -``` - -## Root Config - -This service allows you to read configuration values out of your `app-config` YAML files. - -### Using the service - -The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - log: coreServices.logger, - config: coreServices.rootConfig, - }, - async init({ log, config }) { - const baseUrl = config.getString('backend.baseUrl'); - log.warn(`The backend is running at ${baseUrl}`); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `config` core service. - -- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` -- `remote` - Configure remote configuration loading - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { rootConfigServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - rootConfigServiceFactory({ - argv: [ - '--config', - '/backstage/app-config.development.yaml', - '--config', - '/backstage/app-config.yaml', - ], - remote: { reloadIntervalSeconds: 60 }, - }), -); -``` - -## Logging - -This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. - -### Using the service - -The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - log: coreServices.logger, - }, - async init({ log }) { - log.warn("Here's a nice log line that's a warning!"); - }, - }); - }, -}); -``` - -### Root Logger - -The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. - -If you want to override the implementation for logging across all of the backend, this is the service that you should override. - -### Configuring the service - -The following example is how you can override the root logger service to add additional metadata to all log lines. - -```ts -import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLogger, - deps: { - config: coreServices.rootConfig, - }, - async factory({ config }) { - const logger = WinstonLogger.create({ - meta: { - service: 'backstage', - // here's some additional information that is not part of the - // original implementation - podName: 'myk8spod', - }, - level: process.env.LOG_LEVEL || 'info', - format: - process.env.NODE_ENV === 'production' - ? format.json() - : WinstonLogger.colorFormat(), - transports: [new transports.Console()], - }); - - return logger; - }, - }), -); -``` - -## Cache - -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. - -### 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. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - cache: coreServices.cache, - }, - async init({ cache }) { - const { key, value } = { key: 'test:key', value: 'bob' }; - await cache.set(key, value, { ttl: 1000 }); - - // .. some other stuff. - - await cache.get(key); // 'bob' - }, - }); - }, -}); -``` - -## Database - -This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. - -If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. - -This service is scoped per plugin too, so that table names do not conflict across plugins. - -### Using the service - -The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { resolvePackagePath } from '@backstage/backend-common'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - database: coreServices.database, - }, - async init({ database }) { - const client = await database.getClient(); - const migrationsDir = resolvePackagePath( - '@internal/my-plugin', - 'migrations', - ); - if (!database.migrations?.skip) { - await client.migrate.latest({ - directory: migrationsDir, - }); - } - }, - }); - }, -}); -``` - -## Discovery - -When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. - -### Using the service - -The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { fetch } from 'node-fetch'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - discovery: coreServices.discovery, - }, - async init({ discovery }) { - const url = await discovery.getBaseUrl('derp'); // can also use discovery.getExternalBaseUrl to retrieve external URL - const response = await fetch(`${url}/hello`); - }, - }); - }, -}); -``` - -## Identity - -When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. - -### Using the service - -The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - identity: coreServices.identity, - http: coreServices.httpRouter, - }, - async init({ http, identity }) { - const router = Router(); - router.get('/test-me', (request, response) => { - // use the identity service to pull out the header from the request and get the user - const { - identity: { userEntityRef, ownershipEntityRefs }, - } = await identity.getIdentity({ - request, - }); - - // send the decoded and validated things back to the user - response.json({ - userEntityRef, - ownershipEntityRefs, - }); - }); - - http.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional configuration that you can optionally pass to setup the `identity` core service. - -- `issuer` - Set an optional issuer for validation of the JWT token -- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) - -You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: - -```ts -import { identityServiceFactory } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - identityServiceFactory({ - issuer: 'backstage', - algorithms: ['ES256', 'RS256'], - }), -); -``` - -## Lifecycle - -This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. - -### Using the service - -The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - lifecycle: coreServices.lifecycle, - logger: coreServices.logger, - }, - async init({ lifecycle, logger }) { - // some example work that we want to stop when shutting down - const interval = setInterval(async () => { - await fetch('http://google.com/keepalive').then(r => r.json()); - // do some other stuff. - }, 1000); - - lifecycle.addShutdownHook(() => clearInterval(interval)); - }, - }); - }, -}); -``` - -## Root Lifecycle - -This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. - -### Configure the service - -The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. - -```ts -class MyCustomLifecycleService implements RootLifecycleService { - constructor(private readonly logger: LoggerService) {} - - #isCalled = false; - #shutdownTasks: Array<{ - hook: LifecycleServiceShutdownHook; - options?: LifecycleServiceShutdownOptions; - }> = []; - - addShutdownHook( - hook: LifecycleServiceShutdownHook, - options?: LifecycleServiceShutdownOptions, - ): void { - this.#shutdownTasks.push({ hook, options }); - } - - async shutdown(): Promise { - if (this.#isCalled) { - return; - } - this.#isCalled = true; - - this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); - await Promise.all( - this.#shutdownTasks.map(async ({ hook, options }) => { - const logger = options?.logger ?? this.logger; - try { - await hook(); - logger.info(`Shutdown hook succeeded`); - } catch (error) { - logger.error(`Shutdown hook failed, ${error}`); - } - }), - ); - } -} - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLifecycle, - deps: { - logger: coreServices.rootLogger, - }, - async factory({ logger }) { - return new MyCustomLifecycleService(logger); - }, - }), -); -``` - -## Permissions - -This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. - -### Using the service - -The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - permissions: coreServices.permissions, - http: coreServices.httpRouter, - }, - async init({ permissions, http }) { - const router = Router(); - router.get('/test-me', (request, response) => { - // use the identity service to pull out the token from request headers - const { token } = await identity.getIdentity({ - request, - }); - - // ask the permissions framework what the decision is for the permission - const permissionResponse = await permissions.authorize( - [ - { - permission: myCustomPermission, - }, - ], - { token }, - ); - }); - - http.use(router); - }, - }); - }, -}); -``` - -## Scheduler - -When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. - -### Using the service - -The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { fetch } from 'node-fetch'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - scheduler: coreServices.scheduler, - }, - async init({ scheduler }) { - await scheduler.scheduleTask({ - frequency: { minutes: 10 }, - timeout: { seconds: 30 }, - id: 'ping-google', - fn: async () => { - await fetch('http://google.com/ping'); - }, - }); - }, - }); - }, -}); -``` - -## URL Readers - -Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. - -These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. - -### Using the service - -The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import os from 'os'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - urlReader: coreServices.urlReader, - }, - async init({ urlReader }) { - const buffer = await urlReader - .read('https://github.com/backstage/backstage/blob/master/README.md') - .then(r => r.buffer()); - - const tmpDir = os.tmpdir(); - const directory = await urlReader - .readTree( - 'https://github.com/backstage/backstage/tree/master/packages/backend', - ) - .then(tree => tree.dir({ targetDir: tmpDir })); - }, - }); - }, -}); -``` +## Service Documentation Index + +- [Auth Service](./auth.md) - Token authentication and credentials management. +- [Cache Service](./cache.md) - Key-value store for caching data. +- [Database Service](./database.md) - Database access and management via [knex](https://knexjs.org/). +- [Discovery Service](./discovery.md) - Service discovery for inter-plugin communication. +- [Http Auth Service](./http-auth.md) - Authentication of HTTP requests. +- [Http Router Service](./http-router.md) - HTTP route registration for plugins. +- [Identity Service](./identity.md) - Deprecated user authentication service, use the [Auth Service](./auth.md) instead. +- [Lifecycle Service](./lifecycle.md) - Registration of plugin startup and shutdown lifecycle hooks. +- [Logger Service](./logger.md) - Plugin-level logging. +- [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions. +- [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin. +- [Root Config Service](./root-config.md) - Access to static configuration. +- [Root Http Router Service](./root-http-router.md) - HTTP route registration for root services. +- [Root Lifecycle Service](./root-lifecycle.md) - Registration of backend startup and shutdown lifecycle hooks. +- [Root Logger Service](./root-logger.md) - Root-level logging. +- [Scheduler Service](./scheduler.md) - Scheduling of distributed background tasks. +- [Token Manager Service](./token-manager.md) - Deprecated service authentication service, use the [Auth Service](./auth.md) instead. +- [Url Reader Service](./url-reader.md) - Reading content from external systems. +- [User Info Service](./user-info.md) - Authenticated user information retrieval. diff --git a/docs/backend-system/core-services/auth.md b/docs/backend-system/core-services/auth.md new file mode 100644 index 0000000000..4f1229a9d7 --- /dev/null +++ b/docs/backend-system/core-services/auth.md @@ -0,0 +1,8 @@ +--- +id: auth +title: Auth Service +sidebar_label: Auth +description: Documentation for the Auth service +--- + +TODO diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md new file mode 100644 index 0000000000..0e26e8e1f5 --- /dev/null +++ b/docs/backend-system/core-services/cache.md @@ -0,0 +1,38 @@ +--- +id: cache +title: Cache Service +sidebar_label: Cache +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. + +## 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. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + cache: coreServices.cache, + }, + async init({ cache }) { + const { key, value } = { key: 'test:key', value: 'bob' }; + await cache.set(key, value, { ttl: 1000 }); + + // .. some other stuff. + + await cache.get(key); // 'bob' + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/database.md b/docs/backend-system/core-services/database.md new file mode 100644 index 0000000000..e083f0b44d --- /dev/null +++ b/docs/backend-system/core-services/database.md @@ -0,0 +1,47 @@ +--- +id: database +title: Database Service +sidebar_label: Database +description: Documentation for the Database service +--- + +This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. + +If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. + +This service is scoped per plugin too, so that table names do not conflict across plugins. + +## Using the service + +The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { resolvePackagePath } from '@backstage/backend-common'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + }, + async init({ database }) { + const client = await database.getClient(); + const migrationsDir = resolvePackagePath( + '@internal/my-plugin', + 'migrations', + ); + if (!database.migrations?.skip) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/discovery.md b/docs/backend-system/core-services/discovery.md new file mode 100644 index 0000000000..455e398d8b --- /dev/null +++ b/docs/backend-system/core-services/discovery.md @@ -0,0 +1,35 @@ +--- +id: discovery +title: Discovery Service +sidebar_label: Discovery +description: Documentation for the Discovery service +--- + +When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. + +## Using the service + +The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + discovery: coreServices.discovery, + }, + async init({ discovery }) { + const url = await discovery.getBaseUrl('derp'); // can also use discovery.getExternalBaseUrl to retrieve external URL + const response = await fetch(`${url}/hello`); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md new file mode 100644 index 0000000000..7af30c3251 --- /dev/null +++ b/docs/backend-system/core-services/http-auth.md @@ -0,0 +1,8 @@ +--- +id: http-auth +title: Http Auth Service +sidebar_label: Http Auth +description: Documentation for the Http Auth service +--- + +TODO diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md new file mode 100644 index 0000000000..e976d2c06c --- /dev/null +++ b/docs/backend-system/core-services/http-router.md @@ -0,0 +1,58 @@ +--- +id: http-router +title: Http Router Service +sidebar_label: Http Router +description: Documentation for the Http Router service +--- + +One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. + +## Using the service + +The following example shows how to register a HTTP router for the `example` plugin. +This single route will be available at the `/api/example/hello` path. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { http: coreServices.httpRouter }, + async init({ http }) { + const router = Router(); + router.get('/hello', (_req, res) => { + res.status(200).json({ hello: 'world' }); + }); + // Registers the router at the /api/example path + http.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `httpRouter` core service. + +- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { httpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + httpRouterServiceFactory({ + getPath: (pluginId: string) => `/plugins/${pluginId}`, + }), +); +``` diff --git a/docs/backend-system/core-services/identity.md b/docs/backend-system/core-services/identity.md new file mode 100644 index 0000000000..eb7d1f4ac7 --- /dev/null +++ b/docs/backend-system/core-services/identity.md @@ -0,0 +1,73 @@ +--- +id: identity +title: Identity Service +sidebar_label: Identity +description: Documentation for the Identity service +--- + +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. + +## Using the service + +The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + http: coreServices.httpRouter, + }, + async init({ http, identity }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the header from the request and get the user + const { + identity: { userEntityRef, ownershipEntityRefs }, + } = await identity.getIdentity({ + request, + }); + + // send the decoded and validated things back to the user + response.json({ + userEntityRef, + ownershipEntityRefs, + }); + }); + + http.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `identity` core service. + +- `issuer` - Set an optional issuer for validation of the JWT token +- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { identityServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + identityServiceFactory({ + issuer: 'backstage', + algorithms: ['ES256', 'RS256'], + }), +); +``` diff --git a/docs/backend-system/core-services/lifecycle.md b/docs/backend-system/core-services/lifecycle.md new file mode 100644 index 0000000000..aa547d6cc9 --- /dev/null +++ b/docs/backend-system/core-services/lifecycle.md @@ -0,0 +1,40 @@ +--- +id: lifecycle +title: Lifecycle Service +sidebar_label: Lifecycle +description: Documentation for the Lifecycle service +--- + +This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. + +## Using the service + +The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + lifecycle: coreServices.lifecycle, + logger: coreServices.logger, + }, + async init({ lifecycle, logger }) { + // some example work that we want to stop when shutting down + const interval = setInterval(async () => { + await fetch('http://google.com/keepalive').then(r => r.json()); + // do some other stuff. + }, 1000); + + lifecycle.addShutdownHook(() => clearInterval(interval)); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/logger.md b/docs/backend-system/core-services/logger.md new file mode 100644 index 0000000000..015d2055d5 --- /dev/null +++ b/docs/backend-system/core-services/logger.md @@ -0,0 +1,77 @@ +--- +id: logger +title: Logger Service +sidebar_label: Logger +description: Documentation for the Logger service +--- + +This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. + +## Using the service + +The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + }, + async init({ log }) { + log.warn("Here's a nice log line that's a warning!"); + }, + }); + }, +}); +``` + +## Root Logger + +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +## Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.rootConfig, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + return logger; + }, + }), +); +``` diff --git a/docs/backend-system/core-services/permissions.md b/docs/backend-system/core-services/permissions.md new file mode 100644 index 0000000000..d979932c2f --- /dev/null +++ b/docs/backend-system/core-services/permissions.md @@ -0,0 +1,53 @@ +--- +id: permissions +title: Permissions Service +sidebar_label: Permissions +description: Documentation for the Permissions service +--- + +This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. + +## Using the service + +The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + permissions: coreServices.permissions, + http: coreServices.httpRouter, + }, + async init({ permissions, http }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identity service to pull out the token from request headers + const { token } = await identity.getIdentity({ + request, + }); + + // ask the permissions framework what the decision is for the permission + const permissionResponse = await permissions.authorize( + [ + { + permission: myCustomPermission, + }, + ], + { token }, + ); + }); + + http.use(router); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/plugin-metadata.md b/docs/backend-system/core-services/plugin-metadata.md new file mode 100644 index 0000000000..f045ed7f19 --- /dev/null +++ b/docs/backend-system/core-services/plugin-metadata.md @@ -0,0 +1,8 @@ +--- +id: plugin-metadata +title: Plugin Metadata Service +sidebar_label: Plugin Metadata +description: Documentation for the Plugin Metadata service +--- + +TODO diff --git a/docs/backend-system/core-services/root-config.md b/docs/backend-system/core-services/root-config.md new file mode 100644 index 0000000000..7cdc10f38c --- /dev/null +++ b/docs/backend-system/core-services/root-config.md @@ -0,0 +1,62 @@ +--- +id: root-config +title: Root Config Service +sidebar_label: Root Config +description: Documentation for the Root Config service +--- + +This service allows you to read configuration values out of your `app-config` YAML files. + +## Using the service + +The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + config: coreServices.rootConfig, + }, + async init({ log, config }) { + const baseUrl = config.getString('backend.baseUrl'); + log.warn(`The backend is running at ${baseUrl}`); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional configuration that you can optionally pass to setup the `config` core service. + +- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` +- `remote` - Configure remote configuration loading + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { rootConfigServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + rootConfigServiceFactory({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-http-router.md b/docs/backend-system/core-services/root-http-router.md new file mode 100644 index 0000000000..bbd42b743b --- /dev/null +++ b/docs/backend-system/core-services/root-http-router.md @@ -0,0 +1,76 @@ +--- +id: root-http-router +title: Root Http Router Service +sidebar_label: Root Http Router +description: Documentation for the Root Http Router service +--- + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +## Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async init({ rootHttpRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootHttpRouter.use(router); + }, + }); + }, +}); +``` + +## Configuring the service + +There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; + +const backend = createBackend(); + +backend.add( + rootHttpRouterServiceFactory({ + configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // the built in middleware is provided through an option in the configure function + app.use(middleware.helmet()); + app.use(middleware.cors()); + app.use(middleware.compression()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // here the routes that are registered by other plugins + app.use(routes); + + // some other middleware that comes after the other routes + app.use(middleware.notFound()); + app.use(middleware.error()); + }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-lifecycle.md b/docs/backend-system/core-services/root-lifecycle.md new file mode 100644 index 0000000000..be981e9bd8 --- /dev/null +++ b/docs/backend-system/core-services/root-lifecycle.md @@ -0,0 +1,65 @@ +--- +id: root-lifecycle +title: Root Lifecycle Service +sidebar_label: Root Lifecycle +description: Documentation for the Root Lifecycle service +--- + +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. + +## Configure the service + +The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. + +```ts +class MyCustomLifecycleService implements RootLifecycleService { + constructor(private readonly logger: LoggerService) {} + + #isCalled = false; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; + + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); + } + + async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); + await Promise.all( + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.info(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new MyCustomLifecycleService(logger); + }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md new file mode 100644 index 0000000000..09b7929efe --- /dev/null +++ b/docs/backend-system/core-services/root-logger.md @@ -0,0 +1,8 @@ +--- +id: root-logger +title: Root Logger Service +sidebar_label: Root Logger +description: Documentation for the Root Logger service +--- + +TODO diff --git a/docs/backend-system/core-services/scheduler.md b/docs/backend-system/core-services/scheduler.md new file mode 100644 index 0000000000..e4c67a1199 --- /dev/null +++ b/docs/backend-system/core-services/scheduler.md @@ -0,0 +1,41 @@ +--- +id: scheduler +title: Scheduler Service +sidebar_label: Scheduler +description: Documentation for the Scheduler service +--- + +When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. + +## Using the service + +The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + scheduler: coreServices.scheduler, + }, + async init({ scheduler }) { + await scheduler.scheduleTask({ + frequency: { minutes: 10 }, + timeout: { seconds: 30 }, + id: 'ping-google', + fn: async () => { + await fetch('http://google.com/ping'); + }, + }); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/token-manager.md b/docs/backend-system/core-services/token-manager.md new file mode 100644 index 0000000000..b62499462f --- /dev/null +++ b/docs/backend-system/core-services/token-manager.md @@ -0,0 +1,8 @@ +--- +id: token-manager +title: Token Manager Service +sidebar_label: Token Manager +description: Documentation for the Token Manager service +--- + +TODO diff --git a/docs/backend-system/core-services/url-reader.md b/docs/backend-system/core-services/url-reader.md new file mode 100644 index 0000000000..b9616a029e --- /dev/null +++ b/docs/backend-system/core-services/url-reader.md @@ -0,0 +1,47 @@ +--- +id: url-reader +title: Url Reader Service +sidebar_label: Url Reader +description: Documentation for the Url Reader service +--- + +# URL Readers + +Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. + +These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. + +## Using the service + +The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import os from 'os'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + urlReader: coreServices.urlReader, + }, + async init({ urlReader }) { + const buffer = await urlReader + .read('https://github.com/backstage/backstage/blob/master/README.md') + .then(r => r.buffer()); + + const tmpDir = os.tmpdir(); + const directory = await urlReader + .readTree( + 'https://github.com/backstage/backstage/tree/master/packages/backend', + ) + .then(tree => tree.dir({ targetDir: tmpDir })); + }, + }); + }, +}); +``` diff --git a/docs/backend-system/core-services/user-info.md b/docs/backend-system/core-services/user-info.md new file mode 100644 index 0000000000..5ca717ddd5 --- /dev/null +++ b/docs/backend-system/core-services/user-info.md @@ -0,0 +1,8 @@ +--- +id: user-info +title: User Info Service +sidebar_label: User Info +description: Documentation for the User Info service +--- + +TODO diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 146d8db059..40eaf235c6 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -387,7 +387,28 @@ { "type": "category", "label": "Core Services", - "items": ["backend-system/core-services/index"] + "items": [ + "backend-system/core-services/index", + "backend-system/core-services/auth", + "backend-system/core-services/cache", + "backend-system/core-services/database", + "backend-system/core-services/discovery", + "backend-system/core-services/http-auth", + "backend-system/core-services/http-router", + "backend-system/core-services/identity", + "backend-system/core-services/lifecycle", + "backend-system/core-services/logger", + "backend-system/core-services/permissions", + "backend-system/core-services/plugin-metadata", + "backend-system/core-services/root-config", + "backend-system/core-services/root-http-router", + "backend-system/core-services/root-lifecycle", + "backend-system/core-services/root-logger", + "backend-system/core-services/scheduler", + "backend-system/core-services/token-manager", + "backend-system/core-services/url-reader", + "backend-system/core-services/user-info" + ] } ], "New Frontend System": [ From 9aa01eeb0819fddc01d751fffd3c1a3368e5431c Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 12:42:04 +0100 Subject: [PATCH 087/483] Updated BEP 0004 Signed-off-by: bnechyporenko --- .../README.md | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/beps/0004-scaffolder-task-idempotency/README.md b/beps/0004-scaffolder-task-idempotency/README.md index a7e592af39..28b052e288 100644 --- a/beps/0004-scaffolder-task-idempotency/README.md +++ b/beps/0004-scaffolder-task-idempotency/README.md @@ -4,7 +4,8 @@ status: provisional authors: - 'bnechyporenko@bol.com' - 'benjaminl@spotify.com' -owners: +owners: + - @backstage/scaffolder-maintainers project-areas: - scaffolder creation-date: 2024-01-31 @@ -153,7 +154,7 @@ export function createGithubRepoCreateAction(options: { username: owner, }); - await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { + await ctx.checkpoint('repo.creation', async () => { const repoCreationPromise = user.data.type === 'Organization' ? client.rest.repos.createInOrg({ @@ -168,19 +169,16 @@ export function createGithubRepoCreateAction(options: { }); if (secrets) { - await ctx.checkpoint( - 'v1.task.checkpoint.repo.create.variables', - async () => { - for (const [key, value] of Object.entries(repoVariables ?? {})) { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, - }); - } - }, - ); + await ctx.checkpoint('repo.create.variables', async () => { + for (const [key, value] of Object.entries(repoVariables ?? {})) { + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, + }); + } + }); } ctx.output('remoteUrl', newRepo.clone_url); @@ -204,7 +202,7 @@ Checkpoints will allow action authors to create actions where code paths are ign This will be provided on a context object and action of author provide a key and a callback. ```typescript -await ctx.checkpoint('v1.task.checkpoint.repo.creation', async () => { +await ctx.checkpoint('repo.creation', async () => { const { repoUrl } = await client.rest.Repository.create({}); return { repoUrl }; }); @@ -215,7 +213,7 @@ It's going look like: ```json { - "v1.task.checkpoint.repo.creation": { + "repo.creation": { "status": "success", "result": { "repoUrl": "https://github.com/backstage/backstage.git" @@ -224,6 +222,41 @@ It's going look like: } ``` +or a failed attempt as: + +```json +{ + "repo.creation": { + "status": "failed", + "reason": "Namespace is not valid" + } +} +``` + +DatabaseTaskStore will provide two extra methods `saveTaskState` and `getTaskState`. The type of state in API will be +represented as `JsonObject`. + +Task state will be stored in the extra column `state` in the table `tasks` with the next structure: + +```json +{ + "state": { + "repo.creation": { + "status": "success", + "result": { + "repoUrl": "https://github.com/backstage/backstage.git" + } + }, + "repo.add.member": { + "status": "success", + "result": { + "id": "2345" + } + } + } +} +``` + ## Release Plan + +Add in logic to require comments for specific feedback responses +const overviewContent = ( + +... + +- +- +- +- +- + ... + + ); + + Add in description for 400 response when rating and not authenticated diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md index c405bd4083..4878b2e01e 100644 --- a/plugins/entity-feedback/README.md +++ b/plugins/entity-feedback/README.md @@ -90,6 +90,24 @@ const overviewContent = ( ); +// Require comments for specific feedback responses +const overviewContent = ( + + ... ++ ++ ++ ++ ++ + ... + +); ... // Add to each applicable kind/type of entity as desired diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md index 27e56e5282..d5c03685cd 100644 --- a/plugins/entity-feedback/api-report.md +++ b/plugins/entity-feedback/api-report.md @@ -79,6 +79,8 @@ export interface EntityFeedbackResponse { id: string; // (undocumented) label: string; + // (undocumented) + mustComment?: boolean; } // @public (undocumented) From 2bb58bfee261d8eb23fed109bd6288132ca702a7 Mon Sep 17 00:00:00 2001 From: nikolar Date: Tue, 23 Jan 2024 14:00:31 -0800 Subject: [PATCH 284/483] add test Signed-off-by: nikolar --- .../FeedbackResponseDialog.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx index 631738d6f9..620548d1e1 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx @@ -115,4 +115,22 @@ describe('FeedbackResponseDialog', () => { ); }); }); + + it('will not submit "other" without comments', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Incorrect info' }), + ); + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Other (please specify below)' }), + ); + await userEvent.click( + rendered.getByTestId('feedback-response-dialog-submit-button'), + ); + + await waitFor(() => { + expect(feedbackApi.recordResponse).toHaveBeenCalledTimes(0); + }); + }); }); From 3a5ae74da0d12b10754d77a29ffa2f156004332c Mon Sep 17 00:00:00 2001 From: nikolar Date: Tue, 23 Jan 2024 14:32:25 -0800 Subject: [PATCH 285/483] edit changeset Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index adc642db47..924267822e 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -6,23 +6,21 @@ Add in logic to require comments for specific feedback responses -const overviewContent = ( +const requireComments = ( ... + -- -- -- -- - ... - - ); - - Add in description for 400 response when rating and not authenticated +- feedbackDialogResponses = {[ +- { id: 'incorrect', label: 'Incorrect info' }, +- { id: 'missing', label: 'Missing info', mustComment: true }, +- { id: 'other', label: 'Other (please specify below)', mustComment: true }, +- ]} +- /> + ... + + ); + + + +Add in description for 400 response when rating and not authenticated From 0b90651d0a65a4d85e665f8411b2f95db206528e Mon Sep 17 00:00:00 2001 From: nikolar Date: Tue, 23 Jan 2024 17:33:01 -0800 Subject: [PATCH 286/483] add mustComment to accepted words Signed-off-by: nikolar --- .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 c38a115fa3..f4afe605f1 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -236,6 +236,7 @@ Monorepo monorepos msgraph msw +mustComment mutex mutexes mysql From 20c445b18fad03e733f1ad2ae377711550c063a7 Mon Sep 17 00:00:00 2001 From: nikolar Date: Tue, 23 Jan 2024 18:23:26 -0800 Subject: [PATCH 287/483] remove changes from accept and try backticks Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 4 ++-- .github/vale/config/vocabularies/Backstage/accept.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index 924267822e..52cc34d675 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -13,8 +13,8 @@ const requireComments = ( - feedbackDialogResponses = {[ - { id: 'incorrect', label: 'Incorrect info' }, -- { id: 'missing', label: 'Missing info', mustComment: true }, -- { id: 'other', label: 'Other (please specify below)', mustComment: true }, +- { id: 'missing', label: 'Missing info', `mustComment`: true }, +- { id: 'other', label: 'Other (please specify below)', `mustComment`: true }, - ]} - /> ... diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index f4afe605f1..c38a115fa3 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -236,7 +236,6 @@ Monorepo monorepos msgraph msw -mustComment mutex mutexes mysql From 8ac7843c7e03c3ade6d797aae24d7f20f4b33baa Mon Sep 17 00:00:00 2001 From: nikolar Date: Wed, 24 Jan 2024 09:56:23 -0800 Subject: [PATCH 288/483] improve variable names and update docs Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 2 +- plugins/entity-feedback/README.md | 2 +- .../FeedbackResponseDialog.tsx | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index 52cc34d675..5ba02e2184 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -5,7 +5,7 @@ -Add in logic to require comments for specific feedback responses +Add in logic to link the feedback comment box to specific feedback responses const requireComments = ( ... diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md index 4878b2e01e..b66cf6f567 100644 --- a/plugins/entity-feedback/README.md +++ b/plugins/entity-feedback/README.md @@ -90,7 +90,7 @@ const overviewContent = ( ); -// Require comments for specific feedback responses +// Link the feedback comment box to specific feedback responses const overviewContent = ( ... diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx index 4e98f3b8d8..1acf58003a 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx @@ -97,7 +97,7 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { .filter(r => r.mustComment) .map(r => r.id); - const isMandatedBoxChecked = () => { + const isLinkedBoxChecked = () => { const checkedBoxes = Object.keys(responseSelections).filter( id => responseSelections[id], ); @@ -106,7 +106,7 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { const [{ loading: saving }, saveResponse] = useAsyncFn(async () => { if (requireComments.length > 0) { - if (comments.length === 0 && isMandatedBoxChecked()) { + if (comments.length === 0 && isLinkedBoxChecked()) { alertApi.post({ message: 'The selected option(s) require a comment. Please provide a comment.', @@ -114,7 +114,7 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { }); return; } - if (comments.length > 0 && !isMandatedBoxChecked()) { + if (comments.length > 0 && !isLinkedBoxChecked()) { alertApi.post({ message: 'Please select the option(s) that require a comment.', severity: 'info', @@ -136,7 +136,7 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { } }, [comments, consent, entity, feedbackApi, onClose, responseSelections]); - const selectMandatedBox = (res: boolean) => { + const selectLinkedBox = (res: boolean) => { const newResponseSelections = { ...responseSelections }; requireComments.forEach(id => newResponseSelections[id] === res); setResponseSelections(newResponseSelections); @@ -145,9 +145,9 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { const verifyComments = (e: any) => { setComments(e.target.value); if (requireComments.length > 0) { - selectMandatedBox(true); + selectLinkedBox(true); if (e.target.value.length === 0) { - selectMandatedBox(false); + selectLinkedBox(false); } } }; From 4f8f3df8fd39705919436b8570b9ee8d6b90c3df Mon Sep 17 00:00:00 2001 From: nikolar Date: Wed, 21 Feb 2024 10:50:28 -0800 Subject: [PATCH 289/483] remove dialog box changes Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 16 +----- plugins/entity-feedback/README.md | 18 ------ plugins/entity-feedback/api-report.md | 2 - .../FeedbackResponseDialog.test.tsx | 18 ------ .../FeedbackResponseDialog.tsx | 56 +------------------ .../FeedbackResponseTable.tsx | 10 ++-- 6 files changed, 8 insertions(+), 112 deletions(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index 5ba02e2184..fbd4d0fec1 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -5,21 +5,7 @@ -Add in logic to link the feedback comment box to specific feedback responses -const requireComments = ( - -... - - ... - - ); +Remove empty Chip in `FeedbackResponseTable.tsx` when there is no response diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md index b66cf6f567..c405bd4083 100644 --- a/plugins/entity-feedback/README.md +++ b/plugins/entity-feedback/README.md @@ -90,24 +90,6 @@ const overviewContent = ( ); -// Link the feedback comment box to specific feedback responses -const overviewContent = ( - - ... -+ -+ -+ -+ -+ - ... - -); ... // Add to each applicable kind/type of entity as desired diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md index d5c03685cd..27e56e5282 100644 --- a/plugins/entity-feedback/api-report.md +++ b/plugins/entity-feedback/api-report.md @@ -79,8 +79,6 @@ export interface EntityFeedbackResponse { id: string; // (undocumented) label: string; - // (undocumented) - mustComment?: boolean; } // @public (undocumented) diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx index 620548d1e1..631738d6f9 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx @@ -115,22 +115,4 @@ describe('FeedbackResponseDialog', () => { ); }); }); - - it('will not submit "other" without comments', async () => { - const rendered = await render(); - - await userEvent.click( - rendered.getByRole('checkbox', { name: 'Incorrect info' }), - ); - await userEvent.click( - rendered.getByRole('checkbox', { name: 'Other (please specify below)' }), - ); - await userEvent.click( - rendered.getByTestId('feedback-response-dialog-submit-button'), - ); - - await waitFor(() => { - expect(feedbackApi.recordResponse).toHaveBeenCalledTimes(0); - }); - }); }); diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx index 1acf58003a..6bd4510d3e 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx @@ -16,12 +16,7 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Progress } from '@backstage/core-components'; -import { - ErrorApiError, - errorApiRef, - useApi, - alertApiRef, -} from '@backstage/core-plugin-api'; +import { ErrorApiError, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { Button, Checkbox, @@ -50,13 +45,12 @@ import { entityFeedbackApiRef } from '../../api'; export interface EntityFeedbackResponse { id: string; label: string; - mustComment?: boolean; } const defaultFeedbackResponses: EntityFeedbackResponse[] = [ { id: 'incorrect', label: 'Incorrect info' }, { id: 'missing', label: 'Missing info' }, - { id: 'other', label: 'Other (please specify below)', mustComment: true }, + { id: 'other', label: 'Other (please specify below)' }, ]; /** @@ -87,41 +81,13 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { const classes = useStyles(); const errorApi = useApi(errorApiRef); const feedbackApi = useApi(entityFeedbackApiRef); - const alertApi = useApi(alertApiRef); const [responseSelections, setResponseSelections] = useState( Object.fromEntries(feedbackDialogResponses.map(r => [r.id, false])), ); const [comments, setComments] = useState(''); const [consent, setConsent] = useState(true); - const requireComments = feedbackDialogResponses - .filter(r => r.mustComment) - .map(r => r.id); - - const isLinkedBoxChecked = () => { - const checkedBoxes = Object.keys(responseSelections).filter( - id => responseSelections[id], - ); - return checkedBoxes.some(id => requireComments.includes(id)); - }; const [{ loading: saving }, saveResponse] = useAsyncFn(async () => { - if (requireComments.length > 0) { - if (comments.length === 0 && isLinkedBoxChecked()) { - alertApi.post({ - message: - 'The selected option(s) require a comment. Please provide a comment.', - severity: 'info', - }); - return; - } - if (comments.length > 0 && !isLinkedBoxChecked()) { - alertApi.post({ - message: 'Please select the option(s) that require a comment.', - severity: 'info', - }); - return; - } - } try { await feedbackApi.recordResponse(stringifyEntityRef(entity), { comments, @@ -136,22 +102,6 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { } }, [comments, consent, entity, feedbackApi, onClose, responseSelections]); - const selectLinkedBox = (res: boolean) => { - const newResponseSelections = { ...responseSelections }; - requireComments.forEach(id => newResponseSelections[id] === res); - setResponseSelections(newResponseSelections); - }; - - const verifyComments = (e: any) => { - setComments(e.target.value); - if (requireComments.length > 0) { - selectLinkedBox(true); - if (e.target.value.length === 0) { - selectLinkedBox(false); - } - } - }; - return ( !saving && onClose()}> {saving && } @@ -188,7 +138,7 @@ export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { label="Additional comments" multiline minRows={2} - onChange={e => verifyComments(e)} + onChange={e => setComments(e.target.value)} variant="outlined" value={comments} /> diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx index 22155f90ce..0c26d7a2ba 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx @@ -80,12 +80,10 @@ export const FeedbackResponseTable = (props: FeedbackResponseTableProps) => { width: '35%', render: (response: ResponseRow) => ( <> - {response.response?.length !== undefined && - response.response?.length > 0 - ? response.response - ?.split(',') - .map(res => ) - : ''} + {response.response?.length && + response.response + ?.split(',') + .map(res => )} ), }, From 45115fd213e7647916b36651349163eea879e1c8 Mon Sep 17 00:00:00 2001 From: nikolar Date: Wed, 21 Feb 2024 10:55:24 -0800 Subject: [PATCH 290/483] fix changesets Signed-off-by: nikolar --- .changeset/cyan-toes-repeat.md | 5 +++++ .changeset/perfect-shoes-arrive.md | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .changeset/cyan-toes-repeat.md diff --git a/.changeset/cyan-toes-repeat.md b/.changeset/cyan-toes-repeat.md new file mode 100644 index 0000000000..c261ed829a --- /dev/null +++ b/.changeset/cyan-toes-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback-backend': patch +--- + +Add in description for 400 response when rating and not authenticated diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index fbd4d0fec1..c494df9320 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -1,12 +1,7 @@ --- '@backstage/plugin-entity-feedback': minor -'@backstage/plugin-entity-feedback-backend': patch --- Remove empty Chip in `FeedbackResponseTable.tsx` when there is no response - - - -Add in description for 400 response when rating and not authenticated From cf72b0fcaa42168c7ca1c2e5cdcd108b2310f342 Mon Sep 17 00:00:00 2001 From: nikolar Date: Wed, 21 Feb 2024 11:12:11 -0800 Subject: [PATCH 291/483] fix changeset Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index c494df9320..5db5f2f9f6 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -4,4 +4,4 @@ -Remove empty Chip in `FeedbackResponseTable.tsx` when there is no response +Remove empty Chip in `FeedbackResponseTable.tsx` when there is no response, and fix typo in Feedback Dialog Box. From 9b40f579190c673558a9fc2f85272d12007a2c29 Mon Sep 17 00:00:00 2001 From: nikolar Date: Thu, 22 Feb 2024 09:02:26 -0800 Subject: [PATCH 292/483] add changeset suggestions Signed-off-by: nikolar --- .changeset/cyan-toes-repeat.md | 2 +- .changeset/perfect-shoes-arrive.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/cyan-toes-repeat.md b/.changeset/cyan-toes-repeat.md index c261ed829a..7963e0cb36 100644 --- a/.changeset/cyan-toes-repeat.md +++ b/.changeset/cyan-toes-repeat.md @@ -2,4 +2,4 @@ '@backstage/plugin-entity-feedback-backend': patch --- -Add in description for 400 response when rating and not authenticated +Add in description for 400 response when encountering an invalid rating request diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index 5db5f2f9f6..eaff65c082 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-entity-feedback': minor +'@backstage/plugin-entity-feedback': patch --- From 08bcdf9bbc0c4d08b7460d90ddd2d361907f0046 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:08:24 +0000 Subject: [PATCH 293/483] chore(deps): update dependency @types/react to v18.2.58 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1d2fe96275..63aee59f18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19375,13 +19375,13 @@ __metadata: linkType: hard "@types/react@npm:^18": - version: 18.2.57 - resolution: "@types/react@npm:18.2.57" + version: 18.2.58 + resolution: "@types/react@npm:18.2.58" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 01e7a3424162468428f3b28acec5e5c6cd1e26775ff605d0f46c883dea2d835924873d36b9ea0b75e40c9593aa78ca56a8ccde66bd58dbf6ecb0dd95af28609d + checksum: 42551e30c8a54161a11b2ecd11406782ddba4472a4471d45034c551295263d56f06234f283526d0c0420352ce9ce9675b2a6c65db7a287d9613643d3ceaaf1f0 languageName: node linkType: hard From 341560e4bff1acd0896662a6d0d96f6a1b1f5118 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:54:42 +0000 Subject: [PATCH 294/483] chore(deps): update dependency eslint to v8.57.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 63aee59f18..5aa82df2e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11162,10 +11162,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.56.0": - version: 8.56.0 - resolution: "@eslint/js@npm:8.56.0" - checksum: 5804130574ef810207bdf321c265437814e7a26f4e6fac9b496de3206afd52f533e09ec002a3be06cd9adcc9da63e727f1883938e663c4e4751c007d5b58e539 +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 315dc65b0e9893e2bff139bddace7ea601ad77ed47b4550e73da8c9c2d2766c7a575c3cddf17ef85b8fd6a36ff34f91729d0dcca56e73ca887c10df91a41b0bb languageName: node linkType: hard @@ -11827,14 +11827,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.13": - version: 0.11.13 - resolution: "@humanwhocodes/config-array@npm:0.11.13" +"@humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": ^2.0.1 - debug: ^4.1.1 + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 minimatch: ^3.0.5 - checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 languageName: node linkType: hard @@ -11845,10 +11845,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.1": - version: 2.0.1 - resolution: "@humanwhocodes/object-schema@npm:2.0.1" - checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45 +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee languageName: node linkType: hard @@ -26925,14 +26925,14 @@ __metadata: linkType: hard "eslint@npm:^8.33.0, eslint@npm:^8.6.0": - version: 8.56.0 - resolution: "eslint@npm:8.56.0" + version: 8.57.0 + resolution: "eslint@npm:8.57.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.4 - "@eslint/js": 8.56.0 - "@humanwhocodes/config-array": ^0.11.13 + "@eslint/js": 8.57.0 + "@humanwhocodes/config-array": ^0.11.14 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 "@ungap/structured-clone": ^1.2.0 @@ -26968,7 +26968,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 883436d1e809b4a25d9eb03d42f584b84c408dbac28b0019f6ea07b5177940bf3cca86208f749a6a1e0039b63e085ee47aca1236c30721e91f0deef5cc5a5136 + checksum: 3a48d7ff85ab420a8447e9810d8087aea5b1df9ef68c9151732b478de698389ee656fd895635b5f2871c89ee5a2652b3f343d11e9db6f8486880374ebc74a2d9 languageName: node linkType: hard From fd61d39bc53ebb98af3f43641f6384f09dff88ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:48:52 +0000 Subject: [PATCH 295/483] fix(deps): update dependency testcontainers to v10 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-08c5b50.md | 5 + packages/backend-test-utils/package.json | 2 +- yarn.lock | 170 ++++++++++++++++------- 3 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 .changeset/renovate-08c5b50.md diff --git a/.changeset/renovate-08c5b50.md b/.changeset/renovate-08c5b50.md new file mode 100644 index 0000000000..9ccefc8f0e --- /dev/null +++ b/.changeset/renovate-08c5b50.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Updated dependency `testcontainers` to `^10.0.0`. diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 4153fba211..34233a6f54 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -56,7 +56,7 @@ "msw": "^1.0.0", "mysql2": "^3.0.0", "pg": "^8.11.3", - "testcontainers": "^8.1.2", + "testcontainers": "^10.0.0", "textextensions": "^5.16.0", "uuid": "^8.0.0" }, diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..99070769d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3490,7 +3490,7 @@ __metadata: mysql2: ^3.0.0 pg: ^8.11.3 supertest: ^6.1.3 - testcontainers: ^8.1.2 + testcontainers: ^10.0.0 textextensions: ^5.16.0 uuid: ^8.0.0 peerDependencies: @@ -17941,15 +17941,6 @@ __metadata: languageName: node linkType: hard -"@types/archiver@npm:^5.3.1": - version: 5.3.4 - resolution: "@types/archiver@npm:5.3.4" - dependencies: - "@types/readdir-glob": "*" - checksum: 4ef27b99091ada9b8f13017d5b9e6d42a439e35a7858b30e040c408e081d98d8db6307b0762500288b5da38cab9823c4756b6abae1fdd2658d42bfb09eb7c5fb - languageName: node - linkType: hard - "@types/archiver@npm:^6.0.0": version: 6.0.2 resolution: "@types/archiver@npm:6.0.2" @@ -18406,13 +18397,13 @@ __metadata: languageName: node linkType: hard -"@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.8": - version: 3.3.23 - resolution: "@types/dockerode@npm:3.3.23" +"@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.21": + version: 3.3.24 + resolution: "@types/dockerode@npm:3.3.24" dependencies: "@types/docker-modem": "*" "@types/node": "*" - checksum: 065e9ae43f13641e0df149335914d10af95e559002efe5a5de8e56df9a21b4309b18dbc04fd4c59a4e893c4fedc7df892db28e9d25457bce0c4fd33d21a67834 + checksum: 00329ba9225f5b57bfc0ba8c4dddb17100ebe13c5192fe8a14fce59eec456d258e814b6c78df26d7d7bb878fc38455f559f278490c9c8efad8f708335643b40e languageName: node linkType: hard @@ -21058,7 +21049,7 @@ __metadata: languageName: node linkType: hard -"archiver@npm:^5.3.1": +"archiver@npm:^5.3.2": version: 5.3.2 resolution: "archiver@npm:5.3.2" dependencies: @@ -21430,10 +21421,10 @@ __metadata: languageName: node linkType: hard -"async-lock@npm:^1.1.0": - version: 1.2.4 - resolution: "async-lock@npm:1.2.4" - checksum: 9b8cf65bb9ac7b58ff95539a03b73d51f64d0aea95cde1ebf787859670a8998f0a5258f118db6b54305bf6ed20cf3a2f923f4dd69a58d472fa78cca436c42342 +"async-lock@npm:^1.1.0, async-lock@npm:^1.4.0": + version: 1.4.1 + resolution: "async-lock@npm:1.4.1" + checksum: 29e70cd892932b7c202437786cedc39ff62123cb6941014739bd3cabd6106326416e9e7c21285a5d1dc042cad239a0f7ec9c44658491ee4a615fd36a21c1d10a languageName: node linkType: hard @@ -21899,6 +21890,41 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": + version: 2.2.0 + resolution: "bare-events@npm:2.2.0" + checksum: b3001d61cbb7e6c91c7e47ed1d5701512f94c68955a88c1fe368ff313ba68f372fd701f422d1604fd6ac6e2237024d99373aa14e43a92696755a1f7ae46a8626 + languageName: node + linkType: hard + +"bare-fs@npm:^2.1.1": + version: 2.2.0 + resolution: "bare-fs@npm:2.2.0" + dependencies: + bare-events: ^2.0.0 + bare-os: ^2.0.0 + bare-path: ^2.0.0 + streamx: ^2.13.0 + checksum: 8832abc6c222bdfc8dcf37253493eefdd153048dd2fd482fe7722d6fea083f9e44574197a47e2b0046057f9fb271078ed799d03663e387ad06d2ab116a64cce4 + languageName: node + linkType: hard + +"bare-os@npm:^2.0.0, bare-os@npm:^2.1.0": + version: 2.2.0 + resolution: "bare-os@npm:2.2.0" + checksum: ed78e2f3ea498e35c7565532ae3aa3b85a7e5e223ab6353de64864823cadff02a2a8b7722e9a6c1a0ff56cb9f21f23ada8e88a085cc0a5d38a7c1bcf65e8f7fd + languageName: node + linkType: hard + +"bare-path@npm:^2.0.0, bare-path@npm:^2.1.0": + version: 2.1.0 + resolution: "bare-path@npm:2.1.0" + dependencies: + bare-os: ^2.1.0 + checksum: 03f260e72bd0ae0df4cd712322a2d3c8c16701ffaa55cf2d517ae62b7f78c64b7ec5bba81ec579367f966472481f5160db282e6663bd0fc8cfb09ebe272d8bba + languageName: node + linkType: hard + "base16@npm:^1.0.0": version: 1.0.0 resolution: "base16@npm:1.0.0" @@ -25476,12 +25502,12 @@ __metadata: languageName: node linkType: hard -"docker-compose@npm:^0.23.17": - version: 0.23.17 - resolution: "docker-compose@npm:0.23.17" +"docker-compose@npm:^0.24.2": + version: 0.24.6 + resolution: "docker-compose@npm:0.24.6" dependencies: - yaml: ^1.10.2 - checksum: c308bf067cabe178d245b3e499119937b1d2a5effdc9fac6227e04be4308a0250ca7bb1471789b3d0492ea2ce83f74e40b7517a9a5cb540a21355a64e4ad5d3c + yaml: ^2.2.2 + checksum: 7926e72d7feb9e7feb9e9d46460e18a61cf759cdb9004d7783b58a815eb22b3fbd6402db903cdb764be289cacb1960ef6d19904a9ee435991d8937b146be590f languageName: node linkType: hard @@ -25509,7 +25535,7 @@ __metadata: languageName: node linkType: hard -"dockerode@npm:^3.3.1": +"dockerode@npm:^3.3.5": version: 3.3.5 resolution: "dockerode@npm:3.3.5" dependencies: @@ -35861,7 +35887,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -38745,12 +38771,23 @@ __metadata: languageName: node linkType: hard -"properties-reader@npm:^2.2.0": - version: 2.2.0 - resolution: "properties-reader@npm:2.2.0" +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: ^4.2.4 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 + languageName: node + linkType: hard + +"properties-reader@npm:^2.3.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" dependencies: mkdirp: ^1.0.4 - checksum: a5c5684b1e16633cb695f4fef5476a63f43298619381e8f7f609448f3bda32b26d7c9042b57a427a6dedd1c7fdca1a01ccbe8771b4311ed534079b676c64eec7 + checksum: cbf59e862dc507f8ce1f8d7641ed9737119f16a1d4dad8e79f17b303aaca1c6af7d36ddfef0f649cab4d200ba4334ac159af0b238f6978a085f5b1b5126b6cc3 languageName: node linkType: hard @@ -42440,13 +42477,17 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.15.0": - version: 2.15.5 - resolution: "streamx@npm:2.15.5" +"streamx@npm:^2.13.0, streamx@npm:^2.15.0": + version: 2.16.1 + resolution: "streamx@npm:2.16.1" dependencies: + bare-events: ^2.2.0 fast-fifo: ^1.1.0 queue-tick: ^1.0.1 - checksum: 52e0ec94026d67c9e2e2e1090f05e5b138c2f2822462d9a8ef4a4805625a31d103e55ea5267fcd9bfe041374926424e42aec2dda28a85cb9de42c2a16d416d94 + dependenciesMeta: + bare-events: + optional: true + checksum: 6bbb4c38c0ab6ddbe0857d55e72f71288f308f2a9f4413b7b07391cdf9f94232ffc2bbe40a1212d2e09634ecdbd5052b444c73cc8d67ae1c97e2b7e553dad559 languageName: node linkType: hard @@ -43121,7 +43162,7 @@ __metadata: languageName: node linkType: hard -"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": +"tar-fs@npm:^2.0.0": version: 2.1.1 resolution: "tar-fs@npm:2.1.1" dependencies: @@ -43133,6 +43174,23 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^3.0.4": + version: 3.0.5 + resolution: "tar-fs@npm:3.0.5" + dependencies: + bare-fs: ^2.1.1 + bare-path: ^2.1.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: e31c7e3e525fec0afecdec1cac58071809e396187725f2eba442f08a4c5649c8cd6b7ce25982f9a91bb0f055628df47c08177dd2ea4f5dafd3c22f42f8da8f00 + languageName: node + linkType: hard + "tar-fs@npm:~2.0.1": version: 2.0.1 resolution: "tar-fs@npm:2.0.1" @@ -43158,14 +43216,14 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^3.0.0": - version: 3.1.6 - resolution: "tar-stream@npm:3.1.6" +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" dependencies: b4a: ^1.6.4 fast-fifo: ^1.2.0 streamx: ^2.15.0 - checksum: f3627f918581976e954ff03cb8d370551053796b82564f8c7ca8fac84c48e4d042026d0854fc222171a34ff9c682b72fae91be9c9b0a112d4c54f9e4f443e9c5 + checksum: 6393a6c19082b17b8dcc8e7fd349352bb29b4b8bfe1075912b91b01743ba6bb4298f5ff0b499a3bbaf82121830e96a1a59d4f21a43c0df339e54b01789cb8cc6 languageName: node linkType: hard @@ -43315,23 +43373,26 @@ __metadata: languageName: node linkType: hard -"testcontainers@npm:^8.1.2": - version: 8.16.0 - resolution: "testcontainers@npm:8.16.0" +"testcontainers@npm:^10.0.0": + version: 10.7.1 + resolution: "testcontainers@npm:10.7.1" dependencies: "@balena/dockerignore": ^1.0.2 - "@types/archiver": ^5.3.1 - "@types/dockerode": ^3.3.8 - archiver: ^5.3.1 + "@types/dockerode": ^3.3.21 + archiver: ^5.3.2 + async-lock: ^1.4.0 byline: ^5.0.0 debug: ^4.3.4 - docker-compose: ^0.23.17 - dockerode: ^3.3.1 + docker-compose: ^0.24.2 + dockerode: ^3.3.5 get-port: ^5.1.1 - properties-reader: ^2.2.0 + node-fetch: ^2.7.0 + proper-lockfile: ^4.1.2 + properties-reader: ^2.3.0 ssh-remote-port-forward: ^1.0.4 - tar-fs: ^2.1.1 - checksum: 2fb8250591691a4bd86640b53e13236ad507ba9e03ac3043683de5e9dd632bc29d52827c22ccfe2b0d28dec6896cbaa56dcb153ce65f7f74212ddefc204e8d6a + tar-fs: ^3.0.4 + tmp: ^0.2.1 + checksum: 3ecb439914fab1147943d7d97e4021309fa69f3fcdbc153f4cf3fcf1feb03415da402fb914811189a3606409d58de1174910e144fb4c6c7e3510cc2cb59911b2 languageName: node linkType: hard @@ -43474,6 +43535,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: ^3.0.0 + checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" From 863a4f594130ccb94fc43279e6a42a9e3af8c0a7 Mon Sep 17 00:00:00 2001 From: Antonio Ereiz Date: Sat, 24 Feb 2024 22:08:05 +0100 Subject: [PATCH 296/483] refactor Signed-off-by: Antonio Ereiz --- docs/tutorials/setup-opentelemetry.md | 29 ++++------------------- packages/backend/opentelemetry.js | 34 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 packages/backend/opentelemetry.js diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index ac61f12d33..2be81bc719 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -24,7 +24,7 @@ yarn --cwd packages/backend add @opentelemetry/sdk-node \ ## Configure -In your `packages/backend` folder, create an `instrumentation.ts` file. +In your `packages/backend` folder, create an `instrumentation.js` file. ```typescript const { NodeSDK } = require('@opentelemetry/sdk-node'); @@ -48,7 +48,7 @@ const sdk = new NodeSDK({ sdk.start(); ``` -Your probably won't need all the instrumentation inside `getNodeAutoInstrumentations()` so make sure to +You probably won't need all of the instrumentation inside `getNodeAutoInstrumentations()` so make sure to check the [documentation](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) and tweak it properly. It's important to setup the NodeSDK and the automatic instrumentation **before** importing any library. @@ -59,31 +59,10 @@ flag when we start up the application. In your `Dockerfile` add the `--require` flag which points to the `instrumentation.ts` file ```Dockerfile -FROM node:18-bookworm-slim -... -# More functionality goes here -... -WORKDIR /app -RUN chown node:node /app -USER node - -ENV NODE_ENV production - -COPY --chown=node:node .yarn ./.yarn -COPY --chown=node:node .yarnrc.yml ./ # We need the instrumentation file inside the Docker image so we can use it with --require // highlight-add-next-line -COPY --chown=node:node packages/backend/instrumentation.ts ./ - -COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ -RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz - -RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,sharing=locked,uid=1000,gid=1000 \ - yarn workspaces focus --all --production - -COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ -RUN tar xzf bundle.tar.gz && rm bundle.tar.gz +COPY --chown=node:node packages/backend/instrumentation.js ./ // highlight-remove-next-line CMD ["node", "packages/backend", "--config", "app-config.yaml"] @@ -98,7 +77,7 @@ The above configuration will only work in production once your start a Docker co To be able to test locally you can import the `./instrumentation.ts` file at the top (before all imports) of your backend `index.ts` file ```ts -import '../instrumentation.ts' +import '../instrumentation.js' // Other imports ... ``` diff --git a/packages/backend/opentelemetry.js b/packages/backend/opentelemetry.js new file mode 100644 index 0000000000..074fcb8713 --- /dev/null +++ b/packages/backend/opentelemetry.js @@ -0,0 +1,34 @@ +/* + * 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. + */ +const { NodeSDK } = require('@opentelemetry/sdk-node'); +const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node'); +const { + getNodeAutoInstrumentations, +} = require('@opentelemetry/auto-instrumentations-node'); +const { + PeriodicExportingMetricReader, + ConsoleMetricExporter, +} = require('@opentelemetry/sdk-metrics'); + +const sdk = new NodeSDK({ + traceExporter: new ConsoleSpanExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new ConsoleMetricExporter(), + }), + instrumentations: [getNodeAutoInstrumentations()], +}); + +sdk.start(); From 82ca334cd44a0da54fd889b2a0f8b58330fefa40 Mon Sep 17 00:00:00 2001 From: Antonio Ereiz Date: Sat, 24 Feb 2024 22:09:02 +0100 Subject: [PATCH 297/483] remove opentelemetry file Signed-off-by: Antonio Ereiz --- packages/backend/opentelemetry.js | 34 ------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 packages/backend/opentelemetry.js diff --git a/packages/backend/opentelemetry.js b/packages/backend/opentelemetry.js deleted file mode 100644 index 074fcb8713..0000000000 --- a/packages/backend/opentelemetry.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const { NodeSDK } = require('@opentelemetry/sdk-node'); -const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node'); -const { - getNodeAutoInstrumentations, -} = require('@opentelemetry/auto-instrumentations-node'); -const { - PeriodicExportingMetricReader, - ConsoleMetricExporter, -} = require('@opentelemetry/sdk-metrics'); - -const sdk = new NodeSDK({ - traceExporter: new ConsoleSpanExporter(), - metricReader: new PeriodicExportingMetricReader({ - exporter: new ConsoleMetricExporter(), - }), - instrumentations: [getNodeAutoInstrumentations()], -}); - -sdk.start(); From 20cdeebfbe1b82e81975ac47455c22b2ada0cc75 Mon Sep 17 00:00:00 2001 From: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:21:03 +0100 Subject: [PATCH 298/483] quick fix Signed-off-by: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> --- docs/tutorials/setup-opentelemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index 2be81bc719..5a229d83d9 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -74,7 +74,7 @@ CMD ["node", "--require", "./instrumentation.ts", "packages/backend", "--config" The above configuration will only work in production once your start a Docker container from the image. -To be able to test locally you can import the `./instrumentation.ts` file at the top (before all imports) of your backend `index.ts` file +To be able to test locally you can import the `./instrumentation.js` file at the top (before all imports) of your backend `index.ts` file ```ts import '../instrumentation.js' From 1c6667eaf20949ee3b523f01e6653f538a0f981c Mon Sep 17 00:00:00 2001 From: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:21:36 +0100 Subject: [PATCH 299/483] Update docs/tutorials/setup-opentelemetry.md Signed-off-by: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> --- docs/tutorials/setup-opentelemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index 5a229d83d9..6718974d6e 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -67,7 +67,7 @@ COPY --chown=node:node packages/backend/instrumentation.js ./ // highlight-remove-next-line CMD ["node", "packages/backend", "--config", "app-config.yaml"] // highlight-add-next-line -CMD ["node", "--require", "./instrumentation.ts", "packages/backend", "--config", "app-config.yaml"] +CMD ["node", "--require", "./instrumentation.js", "packages/backend", "--config", "app-config.yaml"] ``` ## Run Backstage From b216a9972feacd41781c922befe2b7545f5e39aa Mon Sep 17 00:00:00 2001 From: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:22:04 +0100 Subject: [PATCH 300/483] Update docs/tutorials/setup-opentelemetry.md Signed-off-by: Antonio Ereiz <51959110+SonilPro@users.noreply.github.com> --- docs/tutorials/setup-opentelemetry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index 6718974d6e..976ba4ccfb 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -56,7 +56,7 @@ It's important to setup the NodeSDK and the automatic instrumentation **before** This is why we will use the nodejs [`--require`](https://nodejs.org/api/cli.html#-r---require-module) flag when we start up the application. -In your `Dockerfile` add the `--require` flag which points to the `instrumentation.ts` file +In your `Dockerfile` add the `--require` flag which points to the `instrumentation.js` file ```Dockerfile From 75f686bad9173290b809ceabaf9a2e95f3b659db Mon Sep 17 00:00:00 2001 From: rui ma Date: Sun, 25 Feb 2024 18:53:16 +0800 Subject: [PATCH 301/483] fix: view component url use LowerCase Signed-off-by: rui ma --- .changeset/silver-impalas-run.md | 5 +++++ .../StepFinishImportLocation.tsx | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/silver-impalas-run.md diff --git a/.changeset/silver-impalas-run.md b/.changeset/silver-impalas-run.md new file mode 100644 index 0000000000..22df521680 --- /dev/null +++ b/.changeset/silver-impalas-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Fixed an issue generating a wrong entity link at the end of the import process diff --git a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx index 38372e59ba..cf5427517a 100644 --- a/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx +++ b/plugins/catalog-import/src/components/StepFinishImportLocation/StepFinishImportLocation.tsx @@ -22,7 +22,7 @@ import { EntityListComponent } from '../EntityListComponent'; import { PrepareResult } from '../useImportState'; import { Link } from '@backstage/core-components'; import partition from 'lodash/partition'; -import { CompoundEntityRef } from '@backstage/catalog-model'; +import { CompoundEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -46,7 +46,12 @@ const filterComponentEntity = ( entity.kind.toLocaleLowerCase('en-US'), ) ) { - return entity; + return { + kind: entity.kind.toLocaleLowerCase('en-US'), + namespace: + entity.namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE, + name: entity.name, + }; } } } From 32eee9dd9f5d7ae20f75db73796ae4b9634d34ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 25 Feb 2024 23:13:51 +0100 Subject: [PATCH 302/483] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-test-utils/src/database/startMysqlContainer.ts | 2 +- .../backend-test-utils/src/database/startPostgresContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts index fa99389cbc..2b4c917393 100644 --- a/packages/backend-test-utils/src/database/startMysqlContainer.ts +++ b/packages/backend-test-utils/src/database/startMysqlContainer.ts @@ -54,7 +54,7 @@ export async function startMysqlContainer(image: string) { const container = await new GenericContainer(image) .withExposedPorts(3306) - .withEnv('MYSQL_ROOT_PASSWORD', password) + .withEnvironment({ MYSQL_ROOT_PASSWORD: password }) .withTmpFs({ '/var/lib/mysql': 'rw' }) .start(); diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts index 81358e01d3..7a1c3f89c5 100644 --- a/packages/backend-test-utils/src/database/startPostgresContainer.ts +++ b/packages/backend-test-utils/src/database/startPostgresContainer.ts @@ -54,7 +54,7 @@ export async function startPostgresContainer(image: string) { const container = await new GenericContainer(image) .withExposedPorts(5432) - .withEnv('POSTGRES_PASSWORD', password) + .withEnvironment({ POSTGRES_PASSWORD: password }) .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) .start(); From 9802004e10d4f97bdec40f49e50ea090498c5146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 23 Feb 2024 17:12:27 +0100 Subject: [PATCH 303/483] auth: convert permission-backend to the new auth services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/fifty-insects-yell.md | 5 ++ .changeset/loud-dolls-exist.md | 5 ++ .changeset/sour-olives-carry.md | 5 ++ .changeset/unlucky-jobs-report.md | 7 ++ .../userInfo/userInfoServiceFactory.ts | 7 +- packages/backend-common/api-report.md | 29 +++---- .../src/auth/createLegacyAuthAdapters.test.ts | 18 ++++- .../src/auth/createLegacyAuthAdapters.ts | 61 ++++++++++++--- packages/backend-test-utils/api-report.md | 13 ++++ .../next/services/MockUserInfoService.test.ts | 55 ++++++++++++++ .../src/next/services/MockUserInfoService.ts | 55 ++++++++++++++ .../src/next/services/mockServices.ts | 43 +++++++++++ .../src/next/wiring/TestBackend.ts | 1 + plugins/permission-backend/api-report.md | 15 +++- plugins/permission-backend/package.json | 1 + plugins/permission-backend/src/plugin.ts | 18 ++++- .../PermissionIntegrationClient.test.ts | 76 +++++++++++-------- .../service/PermissionIntegrationClient.ts | 30 ++++++-- .../src/service/router.test.ts | 67 +++++++--------- .../permission-backend/src/service/router.ts | 57 +++++++++++--- yarn.lock | 1 + 21 files changed, 448 insertions(+), 121 deletions(-) create mode 100644 .changeset/fifty-insects-yell.md create mode 100644 .changeset/loud-dolls-exist.md create mode 100644 .changeset/sour-olives-carry.md create mode 100644 .changeset/unlucky-jobs-report.md create mode 100644 packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts create mode 100644 packages/backend-test-utils/src/next/services/MockUserInfoService.ts diff --git a/.changeset/fifty-insects-yell.md b/.changeset/fifty-insects-yell.md new file mode 100644 index 0000000000..feedd844a5 --- /dev/null +++ b/.changeset/fifty-insects-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added `mockServices.userInfo`, which now also automatically is made available in test backends. diff --git a/.changeset/loud-dolls-exist.md b/.changeset/loud-dolls-exist.md new file mode 100644 index 0000000000..5dce8d71f2 --- /dev/null +++ b/.changeset/loud-dolls-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters` diff --git a/.changeset/sour-olives-carry.md b/.changeset/sour-olives-carry.md new file mode 100644 index 0000000000..d0c3bdd6d0 --- /dev/null +++ b/.changeset/sour-olives-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Made the `DefaultUserInfoService` claims check stricter diff --git a/.changeset/unlucky-jobs-report.md b/.changeset/unlucky-jobs-report.md new file mode 100644 index 0000000000..80e5ecbea0 --- /dev/null +++ b/.changeset/unlucky-jobs-report.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-permission-backend': patch +--- + +Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md). + +The `createRouter` function now has an optional `identity` argument, and instead gained the new `auth`, `httpAuth`, and `userInfo` arguments that should be set to the values of those respective `coreServices`. For users of the new backend system, this happens automatically without code changes. diff --git a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts index a74b8b7002..7d3a2af7b5 100644 --- a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts @@ -43,8 +43,11 @@ export class DefaultUserInfoService implements UserInfoService { if (typeof userEntityRef !== 'string') { throw new Error('User entity ref must be a string'); } - if (!Array.isArray(ownershipEntityRefs)) { - throw new Error('Ownership entity refs must be an array'); + if ( + !Array.isArray(ownershipEntityRefs) || + ownershipEntityRefs.some(ref => typeof ref !== 'string') + ) { + throw new Error('Ownership entity refs must be an array of strings'); } return { userEntityRef, ownershipEntityRefs }; diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 1940a79b78..55cb3b34dc 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -67,6 +67,7 @@ import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService as TokenManager } from '@backstage/backend-plugin-api'; import { TransportStreamOptions } from 'winston-transport'; import { UrlReaderService as UrlReader } from '@backstage/backend-plugin-api'; +import { UserInfoService } from '@backstage/backend-plugin-api'; import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; import { Writable } from 'stream'; @@ -239,30 +240,32 @@ export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; httpAuth?: HttpAuthService; + userInfo?: UserInfoService; identity?: IdentityService; tokenManager?: TokenManager; discovery: PluginEndpointDiscovery; }, - TAdapters = TOptions extends { + TAdapters = (TOptions extends { auth?: AuthService; } - ? TOptions extends { - httpAuth?: HttpAuthService; + ? { + auth: AuthService; } + : {}) & + (TOptions extends { + httpAuth?: HttpAuthService; + } ? { - auth: AuthService; httpAuth: HttpAuthService; } - : { - auth: AuthService; + : {}) & + (TOptions extends { + userInfo?: UserInfoService; + } + ? { + userInfo: UserInfoService; } - : TOptions extends { - httpAuth?: HttpAuthService; - } - ? { - httpAuth: HttpAuthService; - } - : 'error: at least one of auth and/or httpAuth must be provided', + : {}), >(options: TOptions): TAdapters; // @public diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts index 7e1f1be858..db4461781b 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts @@ -56,7 +56,22 @@ describe('createLegacyAuthAdapters', () => { expect(ret.httpAuth).toBe(httpAuth); }); - it('should adapt both auth and httpAuth if neither are provided', () => { + it('should pass through userInfo if it provided', () => { + const auth = {}; + const userInfo = {}; + const ret = createLegacyAuthAdapters({ + auth: auth as any, + userInfo: userInfo as any, + tokenManager: mockServices.tokenManager(), + discovery: {} as any, + identity: mockServices.identity(), + }); + + expect(ret.auth).toBe(auth); + expect(ret.userInfo).toBe(userInfo); + }); + + it('should adapt all services if none are provided', () => { const ret = createLegacyAuthAdapters({ auth: undefined, httpAuth: undefined, @@ -68,6 +83,7 @@ describe('createLegacyAuthAdapters', () => { expect(ret).toEqual({ auth: expect.any(Object), httpAuth: expect.any(Object), + userInfo: expect.any(Object), }); }); }); diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 98d21ba559..d12dd8b9fc 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -19,10 +19,12 @@ import { BackstageCredentials, BackstagePrincipalTypes, BackstageServicePrincipal, + BackstageUserInfo, BackstageUserPrincipal, HttpAuthService, IdentityService, TokenManagerService, + UserInfoService, } from '@backstage/backend-plugin-api'; import { ServerTokenManager, TokenManager } from '../tokens'; import { AuthenticationError, NotAllowedError } from '@backstage/errors'; @@ -203,6 +205,35 @@ class HttpAuthCompat implements HttpAuthService { async issueUserCookie(_res: Response): Promise {} } +export class UserInfoCompat implements UserInfoService { + async getUserInfo( + credentials: BackstageCredentials, + ): Promise { + const internalCredentials = toInternalBackstageCredentials(credentials); + if (internalCredentials.principal.type !== 'user') { + throw new Error('Only user credentials are supported'); + } + if (!internalCredentials.token) { + throw new Error('User credentials is unexpectedly missing token'); + } + const { sub: userEntityRef, ent: ownershipEntityRefs = [] } = decodeJwt( + internalCredentials.token, + ); + + if (typeof userEntityRef !== 'string') { + throw new Error('User entity ref must be a string'); + } + if ( + !Array.isArray(ownershipEntityRefs) || + ownershipEntityRefs.some(ref => typeof ref !== 'string') + ) { + throw new Error('Ownership entity refs must be an array of strings'); + } + + return { userEntityRef, ownershipEntityRefs }; + } +} + /** * An adapter that ensures presence of the auth and/or httpAuth services. * @public @@ -211,38 +242,47 @@ export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; httpAuth?: HttpAuthService; + userInfo?: UserInfoService; identity?: IdentityService; tokenManager?: TokenManager; discovery: PluginEndpointDiscovery; }, - TAdapters = TOptions extends { - auth?: AuthService; - } - ? TOptions extends { httpAuth?: HttpAuthService } - ? { auth: AuthService; httpAuth: HttpAuthService } - : { auth: AuthService } - : TOptions extends { httpAuth?: HttpAuthService } - ? { httpAuth: HttpAuthService } - : 'error: at least one of auth and/or httpAuth must be provided', + TAdapters = (TOptions extends { auth?: AuthService } + ? { auth: AuthService } + : {}) & + (TOptions extends { httpAuth?: HttpAuthService } + ? { httpAuth: HttpAuthService } + : {}) & + (TOptions extends { userInfo?: UserInfoService } + ? { userInfo: UserInfoService } + : {}), >(options: TOptions): TAdapters { - const { auth, httpAuth, discovery } = options; + const { + auth, + httpAuth, + userInfo = new UserInfoCompat(), + discovery, + } = options; if (auth && httpAuth) { return { auth, httpAuth, + userInfo, } as TAdapters; } if (auth) { return { auth, + userInfo, } as TAdapters; } if (httpAuth) { return { httpAuth, + userInfo, } as TAdapters; } @@ -257,5 +297,6 @@ export function createLegacyAuthAdapters< return { auth: authImpl, httpAuth: httpAuthImpl, + userInfo, } as TAdapters; } diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a9ebb402c4..d0e37a476d 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -12,6 +12,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { BackstageNonePrincipal } from '@backstage/backend-plugin-api'; import { BackstageServicePrincipal } from '@backstage/backend-plugin-api'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; @@ -37,6 +38,7 @@ import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; +import { UserInfoService } from '@backstage/backend-plugin-api'; // @public export function createMockDirectory( @@ -316,6 +318,17 @@ export namespace mockServices { partialImpl?: Partial | undefined, ) => ServiceMock; } + export function userInfo( + customInfo?: Partial, + ): UserInfoService; + // (undocumented) + export namespace userInfo { + const factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } } // @public diff --git a/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts b/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts new file mode 100644 index 0000000000..13c8a43213 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockUserInfoService.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { MockUserInfoService } from './MockUserInfoService'; +import { mockCredentials } from './mockCredentials'; + +describe('MockUserInfoService', () => { + it('works without constructor parameters', async () => { + const service = new MockUserInfoService(); + const user = mockCredentials.user(); + await expect(service.getUserInfo(user)).resolves.toEqual({ + userEntityRef: user.principal.userEntityRef, + ownershipEntityRefs: [user.principal.userEntityRef], + }); + }); + + it('works with custom constructor parameters', async () => { + const service = new MockUserInfoService({ + userEntityRef: 'user:default/not-the-mock-1', + ownershipEntityRefs: ['user:default/not-the-mock-2'], + }); + const user = mockCredentials.user(); + await expect(service.getUserInfo(user)).resolves.toEqual({ + userEntityRef: 'user:default/not-the-mock-1', + ownershipEntityRefs: ['user:default/not-the-mock-2'], + }); + }); + + it('rejects non-users', async () => { + const service = new MockUserInfoService(); + await expect( + service.getUserInfo(mockCredentials.none()), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"User info not available for principal type 'none'"`, + ); + await expect( + service.getUserInfo(mockCredentials.service()), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"User info not available for principal type 'service'"`, + ); + }); +}); diff --git a/packages/backend-test-utils/src/next/services/MockUserInfoService.ts b/packages/backend-test-utils/src/next/services/MockUserInfoService.ts new file mode 100644 index 0000000000..68c2a8acae --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockUserInfoService.ts @@ -0,0 +1,55 @@ +/* + * 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 { + BackstageCredentials, + BackstageNonePrincipal, + BackstageServicePrincipal, + BackstageUserInfo, + BackstageUserPrincipal, + UserInfoService, +} from '@backstage/backend-plugin-api'; +import { InputError } from '@backstage/errors'; + +/** @internal */ +export class MockUserInfoService implements UserInfoService { + private readonly customInfo: Partial; + + constructor(customInfo?: Partial) { + this.customInfo = customInfo ?? {}; + } + + async getUserInfo( + credentials: BackstageCredentials, + ): Promise { + const principal = credentials.principal as + | BackstageUserPrincipal + | BackstageServicePrincipal + | BackstageNonePrincipal; + + if (principal.type !== 'user') { + throw new InputError( + `User info not available for principal type '${principal.type}'`, + ); + } + + return { + userEntityRef: principal.userEntityRef, + ownershipEntityRefs: [principal.userEntityRef], + ...this.customInfo, + }; + } +} diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index f72f229434..7300b34105 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -27,6 +27,8 @@ import { DiscoveryService, HttpAuthService, BackstageCredentials, + BackstageUserInfo, + UserInfoService, } from '@backstage/backend-plugin-api'; import { cacheServiceFactory, @@ -49,6 +51,7 @@ import { MockRootLoggerService } from './MockRootLoggerService'; import { MockAuthService } from './MockAuthService'; import { MockHttpAuthService } from './MockHttpAuthService'; import { mockCredentials } from './mockCredentials'; +import { MockUserInfoService } from './MockUserInfoService'; /** @internal */ function simpleFactory< @@ -272,6 +275,37 @@ export namespace mockServices { })); } + /** + * Creates a mock implementation of the `UserInfoService`. + * + * By default it extracts the user's entity ref from a user principal and + * returns that as the only ownership entity ref, but this can be overridden + * by passing in a custom set of user info. + */ + export function userInfo( + customInfo?: Partial, + ): UserInfoService { + return new MockUserInfoService(customInfo); + } + export namespace userInfo { + /** + * Creates a mock service factory for the `UserInfoService`. + * + * By default it extracts the user's entity ref from a user principal and + * returns that as the only ownership entity ref. + */ + export const factory = createServiceFactory({ + service: coreServices.userInfo, + deps: {}, + factory() { + return new MockUserInfoService(); + }, + }); + export const mock = simpleMock(coreServices.userInfo, () => ({ + getUserInfo: jest.fn(), + })); + } + // TODO(Rugvip): Not all core services have implementations available here yet. // some may need a bit more refactoring for it to be simpler to // re-implement functioning mock versions here. @@ -284,12 +318,14 @@ export namespace mockServices { withOptions: jest.fn(), })); } + export namespace database { export const factory = databaseServiceFactory; export const mock = simpleMock(coreServices.database, () => ({ getClient: jest.fn(), })); } + export namespace httpRouter { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ @@ -297,12 +333,14 @@ export namespace mockServices { addAuthPolicy: jest.fn(), })); } + export namespace rootHttpRouter { export const factory = rootHttpRouterServiceFactory; export const mock = simpleMock(coreServices.rootHttpRouter, () => ({ use: jest.fn(), })); } + export namespace lifecycle { export const factory = lifecycleServiceFactory; export const mock = simpleMock(coreServices.lifecycle, () => ({ @@ -310,6 +348,7 @@ export namespace mockServices { addStartupHook: jest.fn(), })); } + export namespace logger { export const factory = loggerServiceFactory; export const mock = simpleMock(coreServices.logger, () => ({ @@ -320,6 +359,7 @@ export namespace mockServices { warn: jest.fn(), })); } + export namespace permissions { export const factory = permissionsServiceFactory; export const mock = simpleMock(coreServices.permissions, () => ({ @@ -327,6 +367,7 @@ export namespace mockServices { authorizeConditional: jest.fn(), })); } + export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; export const mock = simpleMock(coreServices.rootLifecycle, () => ({ @@ -334,6 +375,7 @@ export namespace mockServices { addStartupHook: jest.fn(), })); } + export namespace scheduler { export const factory = schedulerServiceFactory; export const mock = simpleMock(coreServices.scheduler, () => ({ @@ -343,6 +385,7 @@ export namespace mockServices { triggerTask: jest.fn(), })); } + export namespace urlReader { export const factory = urlReaderServiceFactory; export const mock = simpleMock(coreServices.urlReader, () => ({ diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 72a2ed8edb..6b58ebc5ea 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -80,6 +80,7 @@ export const defaultServiceFactories = [ mockServices.rootLogger.factory(), mockServices.scheduler.factory(), mockServices.tokenManager.factory(), + mockServices.userInfo.factory(), mockServices.urlReader.factory(), ]; diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index e9cc4e6272..8b4336ff23 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -3,27 +3,36 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { UserInfoService } from '@backstage/backend-plugin-api'; // @public export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) config: Config; // (undocumented) - discovery: PluginEndpointDiscovery; + discovery: DiscoveryService; // (undocumented) - identity: IdentityApi; + httpAuth?: HttpAuthService; + // (undocumented) + identity?: IdentityApi; // (undocumented) logger: Logger; // (undocumented) policy: PermissionPolicy; + // (undocumented) + userInfo?: UserInfoService; } ``` diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index cc986c4cc8..4131dc29b9 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -62,6 +62,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", diff --git a/plugins/permission-backend/src/plugin.ts b/plugins/permission-backend/src/plugin.ts index 9cf0024e47..cc7f31dfdd 100644 --- a/plugins/permission-backend/src/plugin.ts +++ b/plugins/permission-backend/src/plugin.ts @@ -55,9 +55,19 @@ export const permissionPlugin = createBackendPlugin({ config: coreServices.rootConfig, logger: coreServices.logger, discovery: coreServices.discovery, - identity: coreServices.identity, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + userInfo: coreServices.userInfo, }, - async init({ http, config, logger, discovery, identity }) { + async init({ + http, + config, + logger, + discovery, + auth, + httpAuth, + userInfo, + }) { const winstonLogger = loggerToWinstonLogger(logger); if (!policies.policy) { throw new Error( @@ -69,9 +79,11 @@ export const permissionPlugin = createBackendPlugin({ await createRouter({ config, discovery, - identity, logger: winstonLogger, policy: policies.policy, + auth, + httpAuth, + userInfo, }), ); }, diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index c7a412547d..a36dadd2b5 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -19,7 +19,7 @@ import { Server } from 'http'; import express, { Router, RequestHandler } from 'express'; import { RestContext, rest } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; import { AuthorizeResult, PermissionCondition, @@ -31,10 +31,12 @@ import { } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { z } from 'zod'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; describe('PermissionIntegrationClient', () => { describe('applyConditions', () => { let server: SetupServer; + const auth = mockServices.auth(); const mockConditions: PermissionCriteria = { not: { @@ -58,7 +60,7 @@ describe('PermissionIntegrationClient', () => { ); const mockBaseUrl = 'http://backstage:9191'; - const discovery: PluginEndpointDiscovery = { + const discovery: DiscoveryService = { async getBaseUrl(pluginId) { return `${mockBaseUrl}/${pluginId}`; }, @@ -70,6 +72,7 @@ describe('PermissionIntegrationClient', () => { const client: PermissionIntegrationClient = new PermissionIntegrationClient( { discovery, + auth, }, ); @@ -91,7 +94,7 @@ describe('PermissionIntegrationClient', () => { }); it('should make a POST request to the correct endpoint', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -104,7 +107,7 @@ describe('PermissionIntegrationClient', () => { }); it('should include a request body', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -132,14 +135,18 @@ describe('PermissionIntegrationClient', () => { }); it('should return the response from the fetch request', async () => { - const response = await client.applyConditions('plugin-1', [ - { - id: '123', - resourceRef: 'testResource1', - resourceType: 'test-resource', - conditions: mockConditions, - }, - ]); + const response = await client.applyConditions( + 'plugin-1', + mockCredentials.none(), + [ + { + id: '123', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + ], + ); expect(response).toEqual( expect.objectContaining([{ id: '123', result: AuthorizeResult.ALLOW }]), @@ -147,7 +154,7 @@ describe('PermissionIntegrationClient', () => { }); it('should not include authorization headers if no token is supplied', async () => { - await client.applyConditions('plugin-1', [ + await client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -161,21 +168,22 @@ describe('PermissionIntegrationClient', () => { }); it('should include correctly-constructed authorization header if token is supplied', async () => { - await client.applyConditions( - 'plugin-1', - [ - { - id: '123', - resourceRef: 'testResource1', - resourceType: 'test-resource', - conditions: mockConditions, - }, - ], - 'Bearer fake-token', - ); + await client.applyConditions('plugin-1', mockCredentials.user(), [ + { + id: '123', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + ]); const request = mockApplyConditionsHandler.mock.calls[0][0]; - expect(request.headers.get('authorization')).toEqual('Bearer fake-token'); + expect(request.headers.get('authorization')).toEqual( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'plugin-1', + }), + ); }); it('should forward response errors', async () => { @@ -186,7 +194,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -194,7 +202,7 @@ describe('PermissionIntegrationClient', () => { conditions: mockConditions, }, ]), - ).rejects.toThrow(/401/i); + ).rejects.toThrow(/401/); }); it('should reject invalid responses', async () => { @@ -207,7 +215,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -234,7 +242,7 @@ describe('PermissionIntegrationClient', () => { ); await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -268,6 +276,7 @@ describe('PermissionIntegrationClient', () => { let server: Server; let client: PermissionIntegrationClient; let routerSpy: RequestHandler; + const auth = mockServices.auth(); beforeAll(async () => { const router = Router(); @@ -319,7 +328,7 @@ describe('PermissionIntegrationClient', () => { server = app.listen(resolve); }); - const discovery: PluginEndpointDiscovery = { + const discovery: DiscoveryService = { async getBaseUrl(pluginId: string) { const listenPort = (server.address()! as AddressInfo).port; @@ -332,6 +341,7 @@ describe('PermissionIntegrationClient', () => { client = new PermissionIntegrationClient({ discovery, + auth, }); }); @@ -348,7 +358,7 @@ describe('PermissionIntegrationClient', () => { it('works for simple conditions', async () => { await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', @@ -367,7 +377,7 @@ describe('PermissionIntegrationClient', () => { it('works for complex criteria', async () => { await expect( - client.applyConditions('plugin-1', [ + client.applyConditions('plugin-1', mockCredentials.none(), [ { id: '123', resourceRef: 'testResource1', diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index 2c31d371c3..7567dc8fbb 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -16,7 +16,6 @@ import fetch from 'node-fetch'; import { z } from 'zod'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthorizeResult, ConditionalPolicyDecision, @@ -25,6 +24,11 @@ import { ApplyConditionsRequestEntry, ApplyConditionsResponseEntry, } from '@backstage/plugin-permission-node'; +import { + AuthService, + BackstageCredentials, + DiscoveryService, +} from '@backstage/backend-plugin-api'; const responseSchema = z.object({ items: z.array( @@ -42,20 +46,30 @@ export type ResourcePolicyDecision = ConditionalPolicyDecision & { }; export class PermissionIntegrationClient { - private readonly discovery: PluginEndpointDiscovery; + private readonly discovery: DiscoveryService; + private readonly auth: AuthService; - constructor(options: { discovery: PluginEndpointDiscovery }) { + constructor(options: { discovery: DiscoveryService; auth: AuthService }) { this.discovery = options.discovery; + this.auth = options.auth; } async applyConditions( pluginId: string, + credentials: BackstageCredentials, decisions: readonly ApplyConditionsRequestEntry[], - authHeader?: string, ): Promise { - const endpoint = `${await this.discovery.getBaseUrl( - pluginId, - )}/.well-known/backstage/permissions/apply-conditions`; + const baseUrl = await this.discovery.getBaseUrl(pluginId); + const endpoint = `${baseUrl}/.well-known/backstage/permissions/apply-conditions`; + + const token = this.auth.isPrincipal(credentials, 'none') + ? undefined + : await this.auth + .getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: pluginId, + }) + .then(t => t.token); const response = await fetch(endpoint, { method: 'POST', @@ -70,7 +84,7 @@ export class PermissionIntegrationClient { ), }), headers: { - ...(authHeader ? { authorization: authHeader } : {}), + ...(token ? { authorization: `Bearer ${token}` } : {}), 'content-type': 'application/json', }, }); diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index 7278835c5c..0da9ecd748 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -26,12 +26,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const mockApplyConditions: jest.MockedFunction< InstanceType['applyConditions'] > = jest.fn( async ( _pluginId: string, + _credentials: BackstageCredentials, decisions: readonly ApplyConditionsRequestEntry[], ) => decisions.map(decision => ({ @@ -65,28 +68,12 @@ describe('createRouter', () => { const router = await createRouter({ config: new ConfigReader({ permission: { enabled: true } }), logger: getVoidLogger(), - discovery: { - getBaseUrl: jest.fn(), - getExternalBaseUrl: jest.fn(), - }, - identity: { - getIdentity: jest.fn(({ request: req }) => { - const token = req.headers.authorization?.replace(/^Bearer[ ]+/, ''); - - if (!token) { - return Promise.resolve(undefined); - } - - return Promise.resolve({ - identity: { - type: 'user', - userEntityRef: 'test-user', - ownershipEntityRefs: ['blah'], - }, - token, - }); - }), - }, + discovery: mockServices.discovery(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth({ + defaultCredentials: mockCredentials.none(), + }), + userInfo: mockServices.userInfo(), policy, }); @@ -163,10 +150,9 @@ describe('createRouter', () => { }); it('resolves identity from the Authorization header', async () => { - const token = 'test-token'; const response = await request(app) .post('/authorize') - .auth(token, { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -190,11 +176,16 @@ describe('createRouter', () => { }, }, { - token: 'test-token', + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'catalog', + }), identity: { type: 'user', - userEntityRef: 'test-user', - ownershipEntityRefs: ['blah'], + userEntityRef: mockCredentials.user().principal.userEntityRef, + ownershipEntityRefs: [ + mockCredentials.user().principal.userEntityRef, + ], }, }, ); @@ -271,7 +262,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -319,6 +310,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -333,11 +325,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['no'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -352,7 +344,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['no'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -401,7 +392,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -467,6 +458,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -481,11 +473,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -500,7 +492,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -542,7 +533,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -589,6 +580,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-1', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -597,11 +589,11 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(mockApplyConditions).toHaveBeenCalledWith( 'plugin-2', + mockCredentials.user(), [ expect.objectContaining({ id: '234', @@ -610,7 +602,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params: ['yes'] }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); @@ -656,7 +647,7 @@ describe('createRouter', () => { const response = await request(app) .post('/authorize') - .auth('test-token', { type: 'bearer' }) + .auth(mockCredentials.user.token(), { type: 'bearer' }) .send({ items: [ { @@ -684,6 +675,7 @@ describe('createRouter', () => { expect(mockApplyConditions).toHaveBeenCalledWith( 'test-plugin', + mockCredentials.user(), [ expect.objectContaining({ id: '123', @@ -698,7 +690,6 @@ describe('createRouter', () => { conditions: { rule: 'test-rule', params }, }), ], - 'Bearer test-token', ); expect(response.status).toEqual(200); diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b7e77fdba9..cd7c71fd87 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -19,8 +19,8 @@ import express, { Request, Response } from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { + createLegacyAuthAdapters, errorHandler, - PluginEndpointDiscovery, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { @@ -46,6 +46,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; import DataLoader from 'dataloader'; import { Config } from '@backstage/config'; +import { + AuthService, + BackstageCredentials, + BackstageNonePrincipal, + BackstageUserPrincipal, + DiscoveryService, + HttpAuthService, + UserInfoService, +} from '@backstage/backend-plugin-api'; const attributesSchema: z.ZodSchema = z.object({ action: z @@ -93,28 +102,51 @@ const evaluatePermissionRequestBatchSchema: z.ZodSchema[], - user: BackstageIdentityResponse | undefined, policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, - authHeader?: string, + credentials: BackstageCredentials< + BackstageNonePrincipal | BackstageUserPrincipal + >, + auth: AuthService, + userInfo: UserInfoService, ): Promise[]> => { const applyConditionsLoaderFor = memoize((pluginId: string) => { return new DataLoader< ApplyConditionsRequestEntry, ApplyConditionsResponseEntry >(batch => - permissionIntegrationClient.applyConditions(pluginId, batch, authHeader), + permissionIntegrationClient.applyConditions(pluginId, credentials, batch), ); }); + let user: BackstageIdentityResponse | undefined; + if (auth.isPrincipal(credentials, 'user')) { + const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', // TODO: unknown at this point + }); + user = { + identity: { + type: 'user', + userEntityRef: credentials.principal.userEntityRef, + ownershipEntityRefs, + }, + token, + }; + } + return Promise.all( requests.map(({ id, resourceRef, ...request }) => policy.handle(request, user).then(decision => { @@ -163,7 +195,8 @@ const handleRequest = async ( export async function createRouter( options: RouterOptions, ): Promise { - const { policy, discovery, identity, config, logger } = options; + const { policy, discovery, config, logger } = options; + const { auth, httpAuth, userInfo } = createLegacyAuthAdapters(options); if (!config.getOptionalBoolean('permission.enabled')) { logger.warn( @@ -173,6 +206,7 @@ export async function createRouter( const permissionIntegrationClient = new PermissionIntegrationClient({ discovery, + auth, }); const router = Router(); @@ -188,7 +222,9 @@ export async function createRouter( req: Request, res: Response, ) => { - const user = await identity.getIdentity({ request: req }); + const credentials = await httpAuth.credentials(req, { + allow: ['user', 'none'], + }); const parseResult = evaluatePermissionRequestBatchSchema.safeParse( req.body, @@ -203,10 +239,11 @@ export async function createRouter( res.json({ items: await handleRequest( body.items, - user, policy, permissionIntegrationClient, - req.header('authorization'), + credentials, + auth, + userInfo, ), }); }, diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..1dc69f711f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7917,6 +7917,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" From 1d6764940b43da524d0b23016f1bd2e18d4a43eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 22:27:10 +0000 Subject: [PATCH 304/483] chore(deps): update dependency @types/pg to v8.11.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..491ee4faab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19186,13 +19186,13 @@ __metadata: linkType: hard "@types/pg@npm:^8.6.6": - version: 8.11.0 - resolution: "@types/pg@npm:8.11.0" + version: 8.11.1 + resolution: "@types/pg@npm:8.11.1" dependencies: "@types/node": "*" pg-protocol: "*" pg-types: ^4.0.1 - checksum: 8ae18abce86a012afdd68b2fb85a9fd0e9529f2dae8ca64311a4804fc8423441d605df51f547170efa4584c6ee9f919b4f5f731d5a6221386c5d04560de4334c + checksum: 3d8672800cc96ffeec934c0f7c652d699a1c5a891804e89b6783325b04c496c08ce32237a93da64e3a83540f0f2c3d20d344313716e6f1ea7f335da38a4fd241 languageName: node linkType: hard From 3cd77cb85593c7749819689cbc6a46f093f137d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 22:28:07 +0000 Subject: [PATCH 305/483] chore(deps): update dependency @types/semver to v7.5.8 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..85c5da11b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19498,9 +19498,9 @@ __metadata: linkType: hard "@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.8, @types/semver@npm:^7.5.0": - version: 7.5.7 - resolution: "@types/semver@npm:7.5.7" - checksum: 5af9b13e3d74d86d4b618f6506ccbded801fb35dbc28608cd5a7bfb8bcac0021dd35ef305a72a0c2a8def0cff60acd706bfee16a9ed1c39a893d2a175e778ea7 + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 languageName: node linkType: hard From 68133666c6154dc4cd346c54582cb3c8795fde3a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 18:38:00 +0100 Subject: [PATCH 306/483] playlist-backend: migrate to support new auth services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/eight-fireants-crash.md | 5 + plugins/playlist-backend/api-report.md | 10 +- .../src/service/DatabaseHandler.ts | 10 +- .../src/service/router.test.ts | 97 +++++++++---------- .../playlist-backend/src/service/router.ts | 43 ++++---- 5 files changed, 88 insertions(+), 77 deletions(-) create mode 100644 .changeset/eight-fireants-crash.md diff --git a/.changeset/eight-fireants-crash.md b/.changeset/eight-fireants-crash.md new file mode 100644 index 0000000000..a4eb903585 --- /dev/null +++ b/.changeset/eight-fireants-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist-backend': patch +--- + +Migrated to support new auth services. diff --git a/plugins/playlist-backend/api-report.md b/plugins/playlist-backend/api-report.md index 40b05c25d4..0a6aa3e3bf 100644 --- a/plugins/playlist-backend/api-report.md +++ b/plugins/playlist-backend/api-report.md @@ -3,19 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; import { Conditions } from '@backstage/plugin-permission-node'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PermissionRule } from '@backstage/plugin-permission-node'; +import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -87,15 +89,19 @@ export default playlistPlugin; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) database: PluginDatabaseManager; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) identity: IdentityApi; // (undocumented) logger: Logger; // (undocumented) - permissions: PermissionEvaluator; + permissions: PermissionsService; } ``` diff --git a/plugins/playlist-backend/src/service/DatabaseHandler.ts b/plugins/playlist-backend/src/service/DatabaseHandler.ts index 6118f22ff9..afcaf92d24 100644 --- a/plugins/playlist-backend/src/service/DatabaseHandler.ts +++ b/plugins/playlist-backend/src/service/DatabaseHandler.ts @@ -15,7 +15,7 @@ */ import { resolvePackagePath } from '@backstage/backend-common'; -import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { Playlist, PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; @@ -108,7 +108,7 @@ export class DatabaseHandler { private playlistColumns = ['id', 'name', 'description', 'owner', 'public']; async listPlaylists( - user: BackstageUserIdentity, + user: BackstageUserPrincipal, filter?: ListPlaylistsFilter, ): Promise { let playlistQuery = this.database>( @@ -177,7 +177,7 @@ export class DatabaseHandler { async getPlaylist( id: string, - user?: BackstageUserIdentity, + user?: BackstageUserPrincipal, ): Promise { const playlist = await this.database>( 'playlists', @@ -263,14 +263,14 @@ export class DatabaseHandler { .del(); } - async followPlaylist(playlistId: string, user: BackstageUserIdentity) { + async followPlaylist(playlistId: string, user: BackstageUserPrincipal) { await this.database('followers') .insert({ playlist_id: playlistId, user_ref: user.userEntityRef }) .onConflict(['playlist_id', 'user_ref']) .ignore(); } - async unfollowPlaylist(playlistId: string, user: BackstageUserIdentity) { + async unfollowPlaylist(playlistId: string, user: BackstageUserPrincipal) { await this.database('followers') .where({ playlist_id: playlistId, user_ref: user.userEntityRef }) .del(); diff --git a/plugins/playlist-backend/src/service/router.test.ts b/plugins/playlist-backend/src/service/router.test.ts index 3f90a8b8af..d28eae9041 100644 --- a/plugins/playlist-backend/src/service/router.test.ts +++ b/plugins/playlist-backend/src/service/router.test.ts @@ -20,13 +20,13 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { permissions } from '@backstage/plugin-playlist-common'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const sampleEntities = [ { @@ -60,10 +60,6 @@ jest.mock('@backstage/catalog-client', () => ({ .mockImplementation(() => ({ getEntities: mockGetEntties })), })); -jest.mock('@backstage/plugin-auth-node', () => ({ - getBearerTokenFromAuthorizationHeader: () => 'token', -})); - const mockConditionFilter = { key: 'test', values: ['test-val'] }; jest.mock('../permissions', () => ({ ...jest.requireActual('../permissions'), @@ -128,17 +124,6 @@ describe('createRouter', () => { authorizeConditional: mockedAuthorizeConditional, }; - const mockUser = { - type: 'user', - ownershipEntityRefs: ['user:default/me', 'group:default/owner'], - userEntityRef: 'user:default/me', - }; - const mockIdentityClient = { - getIdentity: jest - .fn() - .mockImplementation(async () => ({ identity: mockUser })), - } as unknown as IdentityApi; - const discovery: jest.Mocked = { getBaseUrl: jest.fn(), getExternalBaseUrl: jest.fn(), @@ -148,9 +133,11 @@ describe('createRouter', () => { const router = await createRouter({ database: createDatabase(), discovery, - identity: mockIdentityClient, + identity: mockServices.identity(), logger: getVoidLogger(), permissions: mockPermissionEvaluator, + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); @@ -173,7 +160,7 @@ describe('createRouter', () => { expect(mockedAuthorizeConditional).toHaveBeenCalledWith( [{ permission: permissions.playlistListRead }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.listPlaylists).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -182,7 +169,7 @@ describe('createRouter', () => { it('should get playlists correctly', async () => { let response = await request(app).get('/').send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, undefined, ); expect(response.status).toEqual(200); @@ -193,7 +180,7 @@ describe('createRouter', () => { ]); response = await request(app).get('/').send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockConditionFilter, ); expect(response.status).toEqual(200); @@ -205,7 +192,7 @@ describe('createRouter', () => { .get('/?filter=mock=test&filter=foo=bar') .send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockRequestFilter, ); expect(response.status).toEqual(200); @@ -217,9 +204,12 @@ describe('createRouter', () => { response = await request(app) .get('/?filter=mock=test&filter=foo=bar') .send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [mockRequestFilter, mockConditionFilter], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { + allOf: [mockRequestFilter, mockConditionFilter], + }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); }); @@ -228,10 +218,10 @@ describe('createRouter', () => { let response = await request(app).get('/?editable=true').send(); expect(mockedAuthorizeConditional).toHaveBeenCalledWith( [{ permission: permissions.playlistListUpdate }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, undefined, ); expect(response.status).toEqual(200); @@ -241,7 +231,7 @@ describe('createRouter', () => { .get('/?editable=true&filter=mock=test&filter=foo=bar') .send(); expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( - mockUser, + mockCredentials.user().principal, mockRequestFilter, ); expect(response.status).toEqual(200); @@ -251,9 +241,10 @@ describe('createRouter', () => { { result: AuthorizeResult.CONDITIONAL }, ]); response = await request(app).get('/?editable=true').send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [mockConditionFilter, mockConditionFilter], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { allOf: [mockConditionFilter, mockConditionFilter] }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); @@ -263,12 +254,15 @@ describe('createRouter', () => { response = await request(app) .get('/?editable=true&filter=mock=test&filter=foo=bar') .send(); - expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith(mockUser, { - allOf: [ - { allOf: [mockRequestFilter, mockConditionFilter] }, - mockConditionFilter, - ], - }); + expect(mockDbHandler.listPlaylists).toHaveBeenLastCalledWith( + mockCredentials.user().principal, + { + allOf: [ + { allOf: [mockRequestFilter, mockConditionFilter] }, + mockConditionFilter, + ], + }, + ); expect(response.status).toEqual(200); expect(response.body).toEqual([mockPlaylist]); }); @@ -285,7 +279,7 @@ describe('createRouter', () => { expect(mockedAuthorize).toHaveBeenCalledWith( [{ permission: permissions.playlistListCreate }], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.createPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -313,7 +307,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.getPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -323,7 +317,7 @@ describe('createRouter', () => { const response = await request(app).get('/playlist-id').send(); expect(mockDbHandler.getPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); expect(response.body).toEqual(mockPlaylist); @@ -346,7 +340,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.updatePlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -375,7 +369,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.deletePlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -404,7 +398,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.addPlaylistEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -436,7 +430,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.getPlaylistEntities).not.toHaveBeenCalled(); expect(mockGetEntties).not.toHaveBeenCalled(); @@ -463,7 +457,12 @@ describe('createRouter', () => { }, ], }, - { token: 'token' }, + { + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'catalog', + }), + }, ); expect(response.status).toEqual(200); expect(response.body).toEqual(sampleEntities); @@ -486,7 +485,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.removePlaylistEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -518,7 +517,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.followPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -528,7 +527,7 @@ describe('createRouter', () => { const response = await request(app).post('/playlist-id/followers').send(); expect(mockDbHandler.followPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); }); @@ -550,7 +549,7 @@ describe('createRouter', () => { resourceRef: 'playlist-id', }, ], - { token: 'token' }, + { credentials: mockCredentials.user() }, ); expect(mockDbHandler.unfollowPlaylist).not.toHaveBeenCalled(); expect(response.status).toEqual(403); @@ -562,7 +561,7 @@ describe('createRouter', () => { .send(); expect(mockDbHandler.unfollowPlaylist).toHaveBeenCalledWith( 'playlist-id', - mockUser, + mockCredentials.user().principal, ); expect(response.status).toEqual(200); }); diff --git a/plugins/playlist-backend/src/service/router.ts b/plugins/playlist-backend/src/service/router.ts index 0397bff2ff..8fa9983d0b 100644 --- a/plugins/playlist-backend/src/service/router.ts +++ b/plugins/playlist-backend/src/service/router.ts @@ -15,6 +15,7 @@ */ import { + createLegacyAuthAdapters, errorHandler, PluginDatabaseManager, PluginEndpointDiscovery, @@ -22,14 +23,10 @@ import { import { CatalogClient } from '@backstage/catalog-client'; import { parseEntityRef } from '@backstage/catalog-model'; import { NotAllowedError } from '@backstage/errors'; -import { - getBearerTokenFromAuthorizationHeader, - IdentityApi, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizePermissionRequest, AuthorizeResult, - PermissionEvaluator, QueryPermissionRequest, } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; @@ -44,6 +41,11 @@ import { Logger } from 'winston'; import { rules, transformConditions } from '../permissions'; import { DatabaseHandler } from './DatabaseHandler'; import { parseListPlaylistsFilterParams } from './ListPlaylistsFilter'; +import { + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** * @public @@ -53,7 +55,9 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; identity: IdentityApi; logger: Logger; - permissions: PermissionEvaluator; + permissions: PermissionsService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @@ -65,11 +69,12 @@ export async function createRouter( const { database, discovery, - identity, logger, permissions: permissionEvaluator, } = options; + const { auth, httpAuth } = createLegacyAuthAdapters(options); + logger.info('Initializing Playlist backend'); const catalogClient = new CatalogClient({ discoveryApi: discovery }); @@ -81,26 +86,21 @@ export async function createRouter( permission: AuthorizePermissionRequest | QueryPermissionRequest, conditional: boolean = false, ) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); - - const user = await identity.getIdentity({ request }); - if (!user) { - throw new NotAllowedError('Unauthorized'); - } + const credentials = await httpAuth.credentials(request, { + allow: ['user'], + }); const decision = conditional ? ( await permissionEvaluator.authorizeConditional( [permission as QueryPermissionRequest], - { token }, + { credentials }, ) )[0] : ( await permissionEvaluator.authorize( [permission as AuthorizePermissionRequest], - { token }, + { credentials }, ) )[0]; @@ -108,7 +108,7 @@ export async function createRouter( throw new NotAllowedError('Unauthorized'); } - return { decision, user: user.identity }; + return { decision, user: credentials.principal }; }; const permissionIntegrationRouter = createPermissionIntegrationRouter({ @@ -227,9 +227,10 @@ export async function createRouter( }; }); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }); // TODO(kuanpg): entities in this playlist that no longer exist in the catalog will be // excluded from this response, we need a way to clean up these orphaned refs potentially From 6b802a2da2064cd3ad84244f2370f4f4afb67da4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 17:47:52 +0100 Subject: [PATCH 307/483] azure-stes-backend: migrate to support new auth services Signed-off-by: Patrik Oldsberg --- .changeset/healthy-experts-rhyme.md | 5 ++ plugins/azure-sites-backend/api-report.md | 13 ++++- plugins/azure-sites-backend/package.json | 1 + .../azure-sites-backend/src/service/router.ts | 47 +++++++++++-------- .../src/service/standaloneServer.ts | 1 + yarn.lock | 1 + 6 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 .changeset/healthy-experts-rhyme.md diff --git a/.changeset/healthy-experts-rhyme.md b/.changeset/healthy-experts-rhyme.md new file mode 100644 index 0000000000..ca4618f1f5 --- /dev/null +++ b/.changeset/healthy-experts-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-sites-backend': minor +--- + +**BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/plugins/azure-sites-backend/api-report.md b/plugins/azure-sites-backend/api-report.md index 04cd22596a..fa6e8f56c6 100644 --- a/plugins/azure-sites-backend/api-report.md +++ b/plugins/azure-sites-backend/api-report.md @@ -3,14 +3,17 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common'; import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common'; import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; // @public (undocumented) export class AzureSitesApi { @@ -55,14 +58,20 @@ export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) azureSitesApi: AzureSitesApi; // (undocumented) catalogApi: CatalogApi; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) - permissions: PermissionEvaluator; + permissions: PermissionsService; } // (No @packageDocumentation comment for this package) diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index 2e9a45a33c..fdaf8825ae 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -36,6 +36,7 @@ "@azure/arm-resourcegraph": "^4.2.1", "@azure/identity": "^4.0.0", "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", diff --git a/plugins/azure-sites-backend/src/service/router.ts b/plugins/azure-sites-backend/src/service/router.ts index bd5d20c05e..79e4f46c04 100644 --- a/plugins/azure-sites-backend/src/service/router.ts +++ b/plugins/azure-sites-backend/src/service/router.ts @@ -14,17 +14,16 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; -import { - PermissionEvaluator, - AuthorizeResult, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { azureSitesActionPermission, azureSitesPermissions, @@ -34,13 +33,22 @@ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission- import { CatalogApi } from '@backstage/catalog-client'; import { AzureSitesApi } from '../api'; +import { + DiscoveryService, + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** @public */ export interface RouterOptions { logger: Logger; azureSitesApi: AzureSitesApi; catalogApi: CatalogApi; - permissions: PermissionEvaluator; + permissions: PermissionsService; + discovery: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @public */ @@ -48,6 +56,7 @@ export async function createRouter( options: RouterOptions, ): Promise { const { logger, azureSitesApi, permissions, catalogApi } = options; + const { auth, httpAuth } = createLegacyAuthAdapters(options); const permissionIntegrationRouter = createPermissionIntegrationRouter({ permissions: azureSitesPermissions, @@ -73,13 +82,15 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/start', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); + const credentials = await httpAuth.credentials(request); const entityRef = request.body.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', + }); const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (entity) { @@ -101,9 +112,7 @@ export async function createRouter( resourceRef: entityRef, }, ], - { - token, - }, + { credentials }, ) )[0] : undefined; @@ -130,14 +139,16 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/stop', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); + const credentials = await httpAuth.credentials(request); const entityRef = request.body.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', + }); const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (entity) { @@ -160,9 +171,7 @@ export async function createRouter( resourceRef: entityRef, }, ], - { - token, - }, + { credentials }, ) )[0] : undefined; diff --git a/plugins/azure-sites-backend/src/service/standaloneServer.ts b/plugins/azure-sites-backend/src/service/standaloneServer.ts index 202c1a128d..c0961f0359 100644 --- a/plugins/azure-sites-backend/src/service/standaloneServer.ts +++ b/plugins/azure-sites-backend/src/service/standaloneServer.ts @@ -53,6 +53,7 @@ export async function startStandaloneServer( permissions, azureSitesApi: AzureSitesApi.fromConfig(config), catalogApi, + discovery, }); let service = createServiceBuilder(module) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..2089595fc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5001,6 +5001,7 @@ __metadata: "@azure/arm-resourcegraph": ^4.2.1 "@azure/identity": ^4.0.0 "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" From 4dc5b4859d73fb900e631abec5c2e0c08c91f22d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 17:40:06 +0100 Subject: [PATCH 308/483] devtools-backend: migate to support new auth services Signed-off-by: Patrik Oldsberg --- .changeset/slow-readers-clap.md | 5 ++ packages/backend/src/plugins/devtools.ts | 1 + plugins/devtools-backend/api-report.md | 10 +++- plugins/devtools-backend/package.json | 1 + plugins/devtools-backend/src/plugin.ts | 13 +++++- .../src/service/router.test.ts | 2 + .../devtools-backend/src/service/router.ts | 46 ++++++++----------- .../src/service/standaloneServer.ts | 1 + yarn.lock | 1 + 9 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 .changeset/slow-readers-clap.md diff --git a/.changeset/slow-readers-clap.md b/.changeset/slow-readers-clap.md new file mode 100644 index 0000000000..5f67c6cf46 --- /dev/null +++ b/.changeset/slow-readers-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-devtools-backend': minor +--- + +**BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/packages/backend/src/plugins/devtools.ts b/packages/backend/src/plugins/devtools.ts index 8e1767ddb1..bfa29cd72e 100644 --- a/packages/backend/src/plugins/devtools.ts +++ b/packages/backend/src/plugins/devtools.ts @@ -25,5 +25,6 @@ export default async function createPlugin( logger: env.logger, config: env.config, permissions: env.permissions, + discovery: env.discovery, }); } diff --git a/plugins/devtools-backend/api-report.md b/plugins/devtools-backend/api-report.md index b9eb7b5a0c..9bde25bd54 100644 --- a/plugins/devtools-backend/api-report.md +++ b/plugins/devtools-backend/api-report.md @@ -7,10 +7,12 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ConfigInfo } from '@backstage/plugin-devtools-common'; import { DevToolsInfo } from '@backstage/plugin-devtools-common'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; import { ExternalDependency } from '@backstage/plugin-devtools-common'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -37,8 +39,12 @@ export interface RouterOptions { // (undocumented) devToolsBackendApi?: DevToolsBackendApi; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) - permissions: PermissionEvaluator; + permissions: PermissionsService; } ``` diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 3b6f3b60ca..da4ce1ca28 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -54,6 +54,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/minimist": "^1.2.0", "@types/ping": "^0.4.1", diff --git a/plugins/devtools-backend/src/plugin.ts b/plugins/devtools-backend/src/plugin.ts index 685522557e..28447e02cc 100644 --- a/plugins/devtools-backend/src/plugin.ts +++ b/plugins/devtools-backend/src/plugin.ts @@ -35,13 +35,24 @@ export const devtoolsPlugin = createBackendPlugin({ logger: coreServices.logger, permissions: coreServices.permissions, httpRouter: coreServices.httpRouter, + discovery: coreServices.discovery, + httpAuth: coreServices.httpAuth, }, - async init({ config, logger, permissions, httpRouter }) { + async init({ + config, + logger, + permissions, + httpRouter, + discovery, + httpAuth, + }) { httpRouter.use( await createRouter({ config, logger: loggerToWinstonLogger(logger), permissions, + discovery, + httpAuth, }), ); }, diff --git a/plugins/devtools-backend/src/service/router.test.ts b/plugins/devtools-backend/src/service/router.test.ts index f3c78242cf..fe4f176a04 100644 --- a/plugins/devtools-backend/src/service/router.test.ts +++ b/plugins/devtools-backend/src/service/router.test.ts @@ -20,6 +20,7 @@ import express from 'express'; import request from 'supertest'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createRouter } from './router'; +import { mockServices } from '@backstage/backend-test-utils'; const mockedAuthorize: jest.MockedFunction = jest.fn(); @@ -49,6 +50,7 @@ describe('createRouter', () => { ], }, }), + discovery: mockServices.discovery(), permissions: permissionEvaluator, }); app = express().use(router); diff --git a/plugins/devtools-backend/src/service/router.ts b/plugins/devtools-backend/src/service/router.ts index fbeeb5af84..4c964dfbc5 100644 --- a/plugins/devtools-backend/src/service/router.ts +++ b/plugins/devtools-backend/src/service/router.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { devToolsConfigReadPermission, devToolsExternalDependenciesReadPermission, @@ -29,17 +26,26 @@ import { DevToolsBackendApi } from '../api'; import { Logger } from 'winston'; import { NotAllowedError } from '@backstage/errors'; import Router from 'express-promise-router'; -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import express from 'express'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { + DiscoveryService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** @public */ export interface RouterOptions { devToolsBackendApi?: DevToolsBackendApi; logger: Logger; config: Config; - permissions: PermissionEvaluator; + permissions: PermissionsService; + discovery: DiscoveryService; + httpAuth?: HttpAuthService; } /** @public */ @@ -48,6 +54,8 @@ export async function createRouter( ): Promise { const { logger, config, permissions } = options; + const { httpAuth } = createLegacyAuthAdapters(options); + const devToolsBackendApi = options.devToolsBackendApi || new DevToolsBackendApi(logger, config); @@ -64,16 +72,10 @@ export async function createRouter( }); router.get('/info', async (req, response) => { - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - const decision = ( await permissions.authorize( [{ permission: devToolsInfoReadPermission }], - { - token, - }, + { credentials: await httpAuth.credentials(req) }, ) )[0]; @@ -87,16 +89,10 @@ export async function createRouter( }); router.get('/config', async (req, response) => { - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - const decision = ( await permissions.authorize( [{ permission: devToolsConfigReadPermission }], - { - token, - }, + { credentials: await httpAuth.credentials(req) }, ) )[0]; @@ -110,16 +106,10 @@ export async function createRouter( }); router.get('/external-dependencies', async (req, response) => { - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - const decision = ( await permissions.authorize( [{ permission: devToolsExternalDependenciesReadPermission }], - { - token, - }, + { credentials: await httpAuth.credentials(req) }, ) )[0]; diff --git a/plugins/devtools-backend/src/service/standaloneServer.ts b/plugins/devtools-backend/src/service/standaloneServer.ts index 43edb47935..75404e98b2 100644 --- a/plugins/devtools-backend/src/service/standaloneServer.ts +++ b/plugins/devtools-backend/src/service/standaloneServer.ts @@ -50,6 +50,7 @@ export async function startStandaloneServer( logger, config, permissions, + discovery, }); let service = createServiceBuilder(module) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..eda5710259 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6183,6 +6183,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/config": "workspace:^" From d621468d930e91e84b52132ce8afd8b7e8ac2a6f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 14 Feb 2024 01:29:36 +0100 Subject: [PATCH 309/483] tech-insights-backend: added auth service support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/ten-spoons-help.md | 5 +++++ .changeset/thick-pillows-develop.md | 5 +++++ plugins/tech-insights-backend/api-report.md | 3 +++ plugins/tech-insights-backend/src/plugin/plugin.ts | 3 +++ .../src/service/fact/FactRetrieverEngine.test.ts | 7 ++++++- .../entityMetadataFactRetriever.test.ts | 2 ++ .../factRetrievers/entityMetadataFactRetriever.ts | 11 +++++------ .../entityOwnershipFactRetriever.test.ts | 2 ++ .../factRetrievers/entityOwnershipFactRetriever.ts | 11 +++++------ .../fact/factRetrievers/techdocsFactRetriever.test.ts | 2 ++ .../fact/factRetrievers/techdocsFactRetriever.ts | 11 +++++------ .../src/service/techInsightsContextBuilder.ts | 10 ++++++++++ plugins/tech-insights-node/api-report.md | 2 ++ plugins/tech-insights-node/src/facts.ts | 2 ++ 14 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 .changeset/ten-spoons-help.md create mode 100644 .changeset/thick-pillows-develop.md diff --git a/.changeset/ten-spoons-help.md b/.changeset/ten-spoons-help.md new file mode 100644 index 0000000000..5089154424 --- /dev/null +++ b/.changeset/ten-spoons-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-node': minor +--- + +**BREAKING**: The `FactRetrieverContext` type now contains an additional `auth` field. diff --git a/.changeset/thick-pillows-develop.md b/.changeset/thick-pillows-develop.md new file mode 100644 index 0000000000..9be71da55d --- /dev/null +++ b/.changeset/thick-pillows-develop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +--- + +Added support for the new `AuthService`. diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 631e792141..c2121bf655 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; @@ -110,6 +111,8 @@ export interface TechInsightsOptions< CheckType extends TechInsightCheck, CheckResultType extends CheckResult, > { + // (undocumented) + auth?: AuthService; // (undocumented) config: Config; // (undocumented) diff --git a/plugins/tech-insights-backend/src/plugin/plugin.ts b/plugins/tech-insights-backend/src/plugin/plugin.ts index 6da1ac1637..e97a0c98d8 100644 --- a/plugins/tech-insights-backend/src/plugin/plugin.ts +++ b/plugins/tech-insights-backend/src/plugin/plugin.ts @@ -102,6 +102,7 @@ export const techInsightsPlugin = createBackendPlugin({ logger: coreServices.logger, scheduler: coreServices.scheduler, tokenManager: coreServices.tokenManager, + auth: coreServices.auth, }, async init({ config, @@ -111,6 +112,7 @@ export const techInsightsPlugin = createBackendPlugin({ logger, scheduler, tokenManager, + auth, }) { const winstonLogger = loggerToWinstonLogger(logger); const factRetrievers: FactRetrieverRegistration[] = Object.entries( @@ -136,6 +138,7 @@ export const techInsightsPlugin = createBackendPlugin({ persistenceContext, scheduler, tokenManager, + auth, }); httpRouter.use( diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 3bbaf9edf5..a6d485d130 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -32,7 +32,11 @@ import { ServerTokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockServices, +} from '@backstage/backend-test-utils'; import { TaskScheduler } from '@backstage/backend-tasks'; jest.setTimeout(60_000); @@ -140,6 +144,7 @@ describe('FactRetrieverEngine', () => { logger: getVoidLogger(), config: ConfigReader.fromConfigs([]), tokenManager: ServerTokenManager.noop(), + auth: mockServices.auth(), discovery: { getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts index 31400788b5..7c608f60bc 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts @@ -23,6 +23,7 @@ import { import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; import { entityMetadataFactRetriever } from './entityMetadataFactRetriever'; +import { mockServices } from '@backstage/backend-test-utils'; const getEntitiesMock = jest.fn(); jest.mock('@backstage/catalog-client', () => { @@ -104,6 +105,7 @@ const defaultEntityListResponse: GetEntitiesResponse = { const handlerContext = { discovery, logger: getVoidLogger(), + auth: mockServices.auth(), config: ConfigReader.fromConfigs([]), tokenManager: ServerTokenManager.noop(), }; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts index 3103f9bdd5..cca4911350 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts @@ -47,12 +47,11 @@ export const entityMetadataFactRetriever: FactRetriever = { description: 'The entity has tags in metadata', }, }, - handler: async ({ - discovery, - entityFilter, - tokenManager, - }: FactRetrieverContext) => { - const { token } = await tokenManager.getToken(); + handler: async ({ discovery, entityFilter, auth }: FactRetrieverContext) => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts index 7a77ef91c4..1c578486ce 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts @@ -23,6 +23,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { mockServices } from '@backstage/backend-test-utils'; const getEntitiesMock = jest.fn(); jest.mock('@backstage/catalog-client', () => { @@ -104,6 +105,7 @@ const defaultEntityListResponse: GetEntitiesResponse = { const handlerContext = { discovery, logger: getVoidLogger(), + auth: mockServices.auth(), config: ConfigReader.fromConfigs([]), tokenManager: ServerTokenManager.noop(), }; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts index 974f1d30cd..367bf42c09 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts @@ -45,12 +45,11 @@ export const entityOwnershipFactRetriever: FactRetriever = { description: 'The spec.owner field is set and refers to a group', }, }, - handler: async ({ - discovery, - entityFilter, - tokenManager, - }: FactRetrieverContext) => { - const { token } = await tokenManager.getToken(); + handler: async ({ discovery, entityFilter, auth }: FactRetrieverContext) => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts index 0c70f6c9f1..37f82cb4dd 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts @@ -23,6 +23,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { mockServices } from '@backstage/backend-test-utils'; const getEntitiesMock = jest.fn(); jest.mock('@backstage/catalog-client', () => { @@ -104,6 +105,7 @@ const defaultEntityListResponse: GetEntitiesResponse = { const handlerContext = { discovery, logger: getVoidLogger(), + auth: mockServices.auth(), config: ConfigReader.fromConfigs([]), tokenManager: ServerTokenManager.noop(), }; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts index bd07bb3ecc..24f2fc4d6f 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts @@ -43,12 +43,11 @@ export const techdocsFactRetriever: FactRetriever = { description: 'The entity has a TechDocs reference annotation', }, }, - handler: async ({ - discovery, - entityFilter, - tokenManager, - }: FactRetrieverContext) => { - const { token } = await tokenManager.getToken(); + handler: async ({ discovery, entityFilter, auth }: FactRetrieverContext) => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 4f49e0031b..7b6f04b183 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -25,6 +25,7 @@ import { PluginDatabaseManager, PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; import { FactChecker, @@ -37,6 +38,7 @@ import { import { initializePersistenceContext } from './persistence'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { AuthService } from '@backstage/backend-plugin-api'; /** * @public @@ -82,6 +84,7 @@ export interface TechInsightsOptions< database: PluginDatabaseManager; scheduler: PluginTaskScheduler; tokenManager: TokenManager; + auth?: AuthService; } /** @@ -147,6 +150,12 @@ export const buildTechInsightsContext = async < logger, })); + const { auth } = createLegacyAuthAdapters({ + auth: options.auth, + tokenManager, + discovery, + }); + const factRetrieverEngine = await DefaultFactRetrieverEngine.create({ scheduler, repository: persistenceContext.techInsightsStore, @@ -156,6 +165,7 @@ export const buildTechInsightsContext = async < discovery, logger, tokenManager, + auth, }, }); diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 13056b5eaa..bf4ee10dea 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; @@ -66,6 +67,7 @@ export type FactRetrieverContext = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth: AuthService; entityFilter?: | Record[] | Record; diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index 9257ea789e..6d40bf615d 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -22,6 +22,7 @@ import { } from '@backstage/backend-common'; import { FactSchema } from '@backstage/plugin-tech-insights-common'; import { Logger } from 'winston'; +import { AuthService } from '@backstage/backend-plugin-api'; /** * A container for facts. The shape of the fact records needs to correspond to the FactSchema with same `ref` value. @@ -92,6 +93,7 @@ export type FactRetrieverContext = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth: AuthService; entityFilter?: | Record[] | Record; From bb368a598beb1b667181f80f9d44ae201cae6c6d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Feb 2024 13:57:03 +0100 Subject: [PATCH 310/483] search-backend-module-{catalog,explore,techdocs}: migrate to support new auth services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/nice-beans-wait.md | 7 +++++ .../api-report.md | 2 ++ .../DefaultCatalogCollatorFactory.ts | 21 ++++++++++--- .../api-report.md | 2 ++ .../collators/ToolDocumentCollatorFactory.ts | 25 ++++++++-------- .../api-report.md | 4 +++ .../src/alpha.ts | 6 ++++ .../DefaultTechDocsCollatorFactory.ts | 30 +++++++++++++++---- 8 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 .changeset/nice-beans-wait.md diff --git a/.changeset/nice-beans-wait.md b/.changeset/nice-beans-wait.md new file mode 100644 index 0000000000..7cbdbc0fdf --- /dev/null +++ b/.changeset/nice-beans-wait.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-search-backend-module-catalog': patch +'@backstage/plugin-search-backend-module-explore': patch +--- + +Migrated to support new auth services. diff --git a/plugins/search-backend-module-catalog/api-report.md b/plugins/search-backend-module-catalog/api-report.md index 6f5813ca9e..fc19ede2d4 100644 --- a/plugins/search-backend-module-catalog/api-report.md +++ b/plugins/search-backend-module-catalog/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { Config } from '@backstage/config'; @@ -41,6 +42,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // @public (undocumented) export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts index 586346c385..166985f2d5 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; import { CatalogApi, @@ -33,9 +34,11 @@ import { Readable } from 'stream'; import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; import { readCollatorConfigOptions } from './config'; import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; +import { AuthService } from '@backstage/backend-plugin-api'; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { + auth?: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; /** @@ -71,20 +74,26 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { private filter?: GetEntitiesRequest['filter']; private batchSize: number; private readonly catalogClient: CatalogApi; - private tokenManager: TokenManager; private entityTransformer: CatalogCollatorEntityTransformer; + private auth: AuthService; static fromConfig( configRoot: Config, options: DefaultCatalogCollatorFactoryOptions, ) { const configOptions = readCollatorConfigOptions(configRoot); + const { auth: adaptedAuth } = createLegacyAuthAdapters({ + auth: options.auth, + discovery: options.discovery, + tokenManager: options.tokenManager, + }); return new DefaultCatalogCollatorFactory({ locationTemplate: options.locationTemplate ?? configOptions.locationTemplate, filter: options.filter ?? configOptions.filter, batchSize: options.batchSize ?? configOptions.batchSize, entityTransformer: options.entityTransformer, + auth: adaptedAuth, discovery: options.discovery, tokenManager: options.tokenManager, catalogClient: options.catalogClient, @@ -96,17 +105,18 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { filter: GetEntitiesRequest['filter']; batchSize: number; entityTransformer?: CatalogCollatorEntityTransformer; + auth: AuthService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; catalogClient?: CatalogApi; }) { const { + auth, batchSize, discovery, locationTemplate, filter, catalogClient, - tokenManager, entityTransformer, } = options; @@ -115,9 +125,9 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { this.batchSize = batchSize; this.catalogClient = catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.tokenManager = tokenManager; this.entityTransformer = entityTransformer ?? defaultCatalogCollatorEntityTransformer; + this.auth = auth; } async getCollator(): Promise { @@ -125,7 +135,6 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { } private async *execute(): AsyncGenerator { - const { token } = await this.tokenManager.getToken(); let entitiesRetrieved = 0; let moreEntitiesToGet = true; @@ -133,6 +142,10 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // limit (and allow some control over) memory used by the search backend // at index-time. while (moreEntitiesToGet) { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const entities = ( await this.catalogClient.getEntities( { diff --git a/plugins/search-backend-module-explore/api-report.md b/plugins/search-backend-module-explore/api-report.md index 50cda184a9..43788340c0 100644 --- a/plugins/search-backend-module-explore/api-report.md +++ b/plugins/search-backend-module-explore/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { ExploreTool } from '@backstage/plugin-explore-common'; @@ -37,5 +38,6 @@ export type ToolDocumentCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager?: TokenManager; + auth?: AuthService; }; ``` diff --git a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts index bf6f07839b..daf83ec02b 100644 --- a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts +++ b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts @@ -17,7 +17,9 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ExploreTool } from '@backstage/plugin-explore-common'; import { @@ -44,6 +46,7 @@ export type ToolDocumentCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager?: TokenManager; + auth?: AuthService; }; /** @@ -56,12 +59,13 @@ export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { private readonly discovery: PluginEndpointDiscovery; private readonly logger: Logger; - private readonly tokenManager?: TokenManager; + private readonly auth: AuthService; private constructor(options: ToolDocumentCollatorFactoryOptions) { this.discovery = options.discovery; this.logger = options.logger; - this.tokenManager = options.tokenManager; + + this.auth = createLegacyAuthAdapters(options).auth; } static fromConfig( @@ -94,16 +98,13 @@ export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { private async fetchTools() { const baseUrl = await this.discovery.getBaseUrl('explore'); - let headers = {}; - - if (this.tokenManager) { - const { token } = await this.tokenManager.getToken(); - headers = { - Authorization: `Bearer ${token}`, - }; - } - - const response = await fetch(`${baseUrl}/tools`, headers); + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'explore', + }); + const response = await fetch(`${baseUrl}/tools`, { + headers: { Authorization: `Bearer ${token}` }, + }); if (!response.ok) { throw new Error( diff --git a/plugins/search-backend-module-techdocs/api-report.md b/plugins/search-backend-module-techdocs/api-report.md index 592cf5065a..1a8c92e5bc 100644 --- a/plugins/search-backend-module-techdocs/api-report.md +++ b/plugins/search-backend-module-techdocs/api-report.md @@ -5,10 +5,12 @@ ```ts /// +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -44,6 +46,8 @@ export type TechDocsCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth?: AuthService; + httpAuth?: HttpAuthService; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; diff --git a/plugins/search-backend-module-techdocs/src/alpha.ts b/plugins/search-backend-module-techdocs/src/alpha.ts index e8be7cc864..f7a6bb0d36 100644 --- a/plugins/search-backend-module-techdocs/src/alpha.ts +++ b/plugins/search-backend-module-techdocs/src/alpha.ts @@ -74,6 +74,8 @@ export default createBackendModule({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, discovery: coreServices.discovery, tokenManager: coreServices.tokenManager, scheduler: coreServices.scheduler, @@ -83,6 +85,8 @@ export default createBackendModule({ async init({ config, logger, + auth, + httpAuth, discovery, tokenManager, scheduler, @@ -106,6 +110,8 @@ export default createBackendModule({ factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, tokenManager, + auth, + httpAuth, logger: loggerToWinstonLogger(logger), catalogClient: catalog, entityTransformer: transformer, diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts index b76fedbdf9..1234c5c6fa 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery, TokenManager, + createLegacyAuthAdapters, } from '@backstage/backend-common'; import { CatalogApi, @@ -41,6 +42,7 @@ import { Readable } from 'stream'; import { Logger } from 'winston'; import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer'; import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; interface MkSearchIndexDoc { title: string; @@ -57,6 +59,8 @@ export type TechDocsCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; tokenManager: TokenManager; + auth?: AuthService; + httpAuth?: HttpAuthService; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; @@ -84,8 +88,8 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { private discovery: PluginEndpointDiscovery; private locationTemplate: string; private readonly logger: Logger; + private readonly auth: AuthService; private readonly catalogClient: CatalogApi; - private readonly tokenManager: TokenManager; private readonly parallelismLimit: number; private readonly legacyPathCasing: boolean; private entityTransformer: TechDocsCollatorEntityTransformer; @@ -100,9 +104,14 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { new CatalogClient({ discoveryApi: options.discovery }); this.parallelismLimit = options.parallelismLimit ?? 10; this.legacyPathCasing = options.legacyPathCasing ?? false; - this.tokenManager = options.tokenManager; this.entityTransformer = options.entityTransformer ?? defaultTechDocsCollatorEntityTransformer; + + this.auth = createLegacyAuthAdapters({ + auth: options.auth, + discovery: options.discovery, + tokenManager: options.tokenManager, + }).auth; } static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) { @@ -131,7 +140,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { private async *execute(): AsyncGenerator { const limit = pLimit(this.parallelismLimit); const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); - const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; let moreEntitiesToGet = true; @@ -141,6 +150,11 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { // parallelism limit to simplify configuration. const batchSize = this.parallelismLimit * 50; while (moreEntitiesToGet) { + const { token: catalogToken } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + const entities = ( await this.catalogClient.getEntities( { @@ -151,7 +165,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { limit: batchSize, offset: entitiesRetrieved, }, - { token }, + { token: catalogToken }, ) ).items; @@ -174,6 +188,12 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { ); try { + const { token: techdocsToken } = + await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'techdocs', + }); + const searchIndexResponse = await fetch( DefaultTechDocsCollatorFactory.constructDocsIndexUrl( techDocsBaseUrl, @@ -181,7 +201,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { ), { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${techdocsToken}`, }, }, ); From 8efe690204d2a58621aa734c361b728b50e3845b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 19:06:49 +0100 Subject: [PATCH 311/483] code-coverage-backend: migrate to support new auth services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/friendly-coats-travel.md | 5 ++ plugins/code-coverage-backend/api-report.md | 6 +++ plugins/code-coverage-backend/package.json | 1 + .../src/service/router.test.ts | 25 +++++----- .../src/service/router.ts | 46 +++++++++++++------ yarn.lock | 1 + 6 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 .changeset/friendly-coats-travel.md diff --git a/.changeset/friendly-coats-travel.md b/.changeset/friendly-coats-travel.md new file mode 100644 index 0000000000..70d684a3da --- /dev/null +++ b/.changeset/friendly-coats-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Migrated to support new auth services. diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 8a64361391..fef869a302 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -3,10 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -21,6 +23,8 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) catalogApi?: CatalogApi; // (undocumented) @@ -30,6 +34,8 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) urlReader: UrlReader; diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index b3c3aae2d7..acdeab5a23 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -48,6 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser-xml": "^2.0.2", "@types/supertest": "^2.0.8", diff --git a/plugins/code-coverage-backend/src/service/router.test.ts b/plugins/code-coverage-backend/src/service/router.test.ts index 2032a5470d..64ea515ac0 100644 --- a/plugins/code-coverage-backend/src/service/router.test.ts +++ b/plugins/code-coverage-backend/src/service/router.test.ts @@ -26,6 +26,7 @@ import { import { ConfigReader } from '@backstage/config'; import { createRouter } from './router'; import { CatalogRequestOptions } from '@backstage/catalog-client'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; jest.mock('./CodeCoverageDatabase'); @@ -96,6 +97,8 @@ describe('createRouter', () => { discovery: testDiscovery, urlReader: mockUrlReader, logger: getVoidLogger(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); @@ -118,21 +121,21 @@ describe('createRouter', () => { '/history?entity=component:default/mycomponent', ].forEach(uri => { describe(`GET ${uri}`, () => { - it('does not send token when calling catalog api and request is unauthenticated', async () => { - const response = await request(app).get(uri); - - expect(response.status).toEqual(200); - expect(catalogRequestOptions.token).toBeUndefined(); - }); - - it('includes auth token when calling catalog api', async () => { - const token = 'my-auth-token'; + it('forwards request credentials to the catalog api call', async () => { const response = await request(app) .get(uri) - .set('Authorization', `Bearer ${token}`); + .set( + 'Authorization', + mockCredentials.user.header('user:default/other'), + ); expect(response.status).toEqual(200); - expect(catalogRequestOptions.token).toEqual(token); + expect(catalogRequestOptions.token).toEqual( + mockCredentials.service.token({ + onBehalfOf: mockCredentials.user('user:default/other'), + targetPluginId: 'catalog', + }), + ); }); }); }); diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 47bc285d62..d1a0a1703c 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -21,6 +21,7 @@ import BodyParser from 'body-parser'; import bodyParserXml from 'body-parser-xml'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { + createLegacyAuthAdapters, errorHandler, PluginDatabaseManager, PluginEndpointDiscovery, @@ -33,7 +34,7 @@ import { CodeCoverageDatabase } from './CodeCoverageDatabase'; import { aggregateCoverage, CoverageUtils } from './CoverageUtils'; import { Converter, Jacoco, Cobertura, Lcov } from './converter'; import { getEntitySourceLocation } from '@backstage/catalog-model'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; /** * Options for {@link createRouter}. @@ -47,6 +48,8 @@ export interface RouterOptions { urlReader: UrlReader; logger: Logger; catalogApi?: CatalogApi; + auth?: AuthService; + httpAuth?: HttpAuthService; } export interface CodeCoverageApi { @@ -63,6 +66,7 @@ export const makeRouter = async ( const catalogApi = options.catalogApi ?? new CatalogClient({ discoveryApi: discovery }); const scm = ScmIntegrations.fromConfig(config); + const { auth, httpAuth } = createLegacyAuthAdapters(options); const bodySizeLimit = config.getOptionalString('codeCoverage.bodySizeLimit') ?? '100kb'; @@ -92,9 +96,13 @@ export const makeRouter = async ( */ router.get('/report', async (req, res) => { const { entity } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -116,9 +124,13 @@ export const makeRouter = async ( */ router.get('/history', async (req, res) => { const { entity } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -136,9 +148,13 @@ export const makeRouter = async ( */ router.get('/file-content', async (req, res) => { const { entity, path } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entityLookup = await catalogApi.getEntityByRef( + entity as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -189,9 +205,13 @@ export const makeRouter = async ( */ router.post('/report', async (req, res) => { const { entity: entityRef, coverageType } = req.query; - const entity = await catalogApi.getEntityByRef(entityRef as string, { - token: getBearerTokenFromAuthorizationHeader(req.headers.authorization), - }); + const entity = await catalogApi.getEntityByRef( + entityRef as string, + await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }), + ); if (!entity) { throw new NotFoundError(`No entity found matching ${entityRef}`); } diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..08887fd8d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6014,6 +6014,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" From 55191cc4f6f45714b2a31c8713fb684e54432093 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 14 Feb 2024 13:30:38 +0100 Subject: [PATCH 312/483] jenkins-backend: migrated to use auth services Signed-off-by: Patrik Oldsberg --- .changeset/heavy-coats-sniff.md | 7 +++ packages/backend/src/plugins/jenkins.ts | 2 + plugins/jenkins-backend/api-report.md | 16 ++++- plugins/jenkins-backend/package.json | 1 + plugins/jenkins-backend/src/plugin.ts | 16 ++++- plugins/jenkins-backend/src/run.ts | 4 +- .../src/service/jenkinsApi.test.ts | 6 ++ .../jenkins-backend/src/service/jenkinsApi.ts | 15 ++--- .../src/service/jenkinsInfoProvider.test.ts | 59 +++++++++++-------- .../src/service/jenkinsInfoProvider.ts | 26 ++++++-- plugins/jenkins-backend/src/service/router.ts | 38 ++++++------ .../src/service/standaloneServer.ts | 5 +- yarn.lock | 1 + 13 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 .changeset/heavy-coats-sniff.md diff --git a/.changeset/heavy-coats-sniff.md b/.changeset/heavy-coats-sniff.md new file mode 100644 index 0000000000..c503ec0dec --- /dev/null +++ b/.changeset/heavy-coats-sniff.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-jenkins-backend': minor +--- + +**BREAKING**: Both `createRouter` and `DefaultJenkinsInfoProvider.fromConfig` now require the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +The `JenkinsInfoProvider` interface has been updated to receive `credentials` of the type `BackstageCredentials` rather than a token. diff --git a/packages/backend/src/plugins/jenkins.ts b/packages/backend/src/plugins/jenkins.ts index d62200b0ac..7d47ee338d 100644 --- a/packages/backend/src/plugins/jenkins.ts +++ b/packages/backend/src/plugins/jenkins.ts @@ -32,6 +32,8 @@ export default async function createPlugin( jenkinsInfoProvider: DefaultJenkinsInfoProvider.fromConfig({ catalog, config: env.config, + discovery: env.discovery, }), + discovery: env.discovery, }); } diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 28dafa70bf..e1cd04d8ec 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -3,11 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -21,12 +25,14 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static fromConfig(options: { config: Config; catalog: CatalogApi; + discovery: DiscoveryService; + auth?: AuthService; }): DefaultJenkinsInfoProvider; // (undocumented) getInstance(opt: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; // (undocumented) static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; @@ -61,7 +67,7 @@ export interface JenkinsInfoProvider { getInstance(options: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; } @@ -86,6 +92,12 @@ export default jenkinsPlugin; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; + // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; // (undocumented) jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 069e4f7ae8..eaf8218a01 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -50,6 +50,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/jenkins": "^1.0.0", "@types/supertest": "^2.0.8" diff --git a/plugins/jenkins-backend/src/plugin.ts b/plugins/jenkins-backend/src/plugin.ts index 8da26d62aa..c02fc3e189 100644 --- a/plugins/jenkins-backend/src/plugin.ts +++ b/plugins/jenkins-backend/src/plugin.ts @@ -38,12 +38,24 @@ export const jenkinsPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, config: coreServices.rootConfig, catalogClient: catalogServiceRef, + discovery: coreServices.discovery, + auth: coreServices.auth, }, - async init({ logger, permissions, httpRouter, config, catalogClient }) { + async init({ + logger, + permissions, + httpRouter, + config, + catalogClient, + discovery, + auth, + }) { const winstonLogger = loggerToWinstonLogger(logger); const jenkinsInfoProvider = DefaultJenkinsInfoProvider.fromConfig({ + auth, config, catalog: catalogClient, + discovery, }); httpRouter.use( await createRouter({ @@ -56,6 +68,8 @@ export const jenkinsPlugin = createBackendPlugin({ * Info provider to be able to get all necessary information for the APIs */ jenkinsInfoProvider, + discovery, + auth, }), ); }, diff --git a/plugins/jenkins-backend/src/run.ts b/plugins/jenkins-backend/src/run.ts index 0a3ed2b7f0..95ff18a510 100644 --- a/plugins/jenkins-backend/src/run.ts +++ b/plugins/jenkins-backend/src/run.ts @@ -17,12 +17,14 @@ import { getRootLogger } from '@backstage/backend-common'; import yn from 'yn'; import { startStandaloneServer } from './service/standaloneServer'; +import { ConfigReader } from '@backstage/config'; const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); const logger = getRootLogger(); +const config = new ConfigReader({}); -startStandaloneServer({ port, enableCors, logger }).catch(err => { +startStandaloneServer({ config, port, enableCors, logger }).catch(err => { logger.error(err); process.exit(1); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 8e80de34a3..2d4fb1d62e 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -20,6 +20,7 @@ import { JenkinsInfo } from './jenkinsInfoProvider'; import { JenkinsBuild, JenkinsProject } from '../types'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import fetch, { Response } from 'node-fetch'; +import { mockServices } from '@backstage/backend-test-utils'; jest.mock('jenkins'); jest.mock('node-fetch'); @@ -716,6 +717,8 @@ describe('JenkinsApi', () => { ); }); describe('rebuildProject', () => { + const auth = mockServices.auth(); + it('successfully rebuilds', async () => { mockFetch.mockResolvedValueOnce({ status: 200 } as Response); const status = await jenkinsApi.rebuildProject( @@ -723,6 +726,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(200); }); @@ -733,6 +737,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(401); }); @@ -750,6 +755,7 @@ describe('JenkinsApi', () => { jobFullName, buildNumber, resourceRef, + { credentials: await auth.getOwnServiceCredentials() }, ); expect(status).toEqual(401); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 2cf432343f..9171670743 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -23,12 +23,13 @@ import type { JenkinsProject, ScmDetails, } from '../types'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; import fetch, { HeaderInit } from 'node-fetch'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ @@ -75,7 +76,7 @@ export class JenkinsApiImpl { inQueue, builds[*]`; - constructor(private readonly permissionApi?: PermissionEvaluator) {} + constructor(private readonly permissionApi?: PermissionsService) {} /** * Get a list of projects for the given JenkinsInfo. @@ -160,12 +161,12 @@ export class JenkinsApiImpl { jobFullName: string, buildNumber: number, resourceRef: string, - options?: { token?: string }, + options: { credentials: BackstageCredentials }, ): Promise { if (this.permissionApi) { const response = await this.permissionApi.authorize( [{ permission: jenkinsExecutePermission, resourceRef }], - { token: options?.token }, + { credentials: options.credentials }, ); // permission api returns always at least one item, we need to check only one result since we do not expect any additional results const { result } = response[0]; diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 626e5b0a5a..6be3ec572c 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -22,6 +22,7 @@ import { JenkinsConfig, JenkinsInfo, } from './jenkinsInfoProvider'; +import { mockServices } from '@backstage/backend-test-utils'; describe('JenkinsConfig', () => { it('Reads simple config and annotation', async () => { @@ -184,6 +185,8 @@ describe('DefaultJenkinsInfoProvider', () => { return DefaultJenkinsInfoProvider.fromConfig({ config, catalog: mockCatalog, + discovery: mockServices.discovery(), + auth: mockServices.auth(), }); } @@ -191,9 +194,10 @@ describe('DefaultJenkinsInfoProvider', () => { const provider = configureProvider({ jenkins: {} }, undefined); await expect(provider.getInstance({ entityRef })).rejects.toThrow(); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); }); it('Reads simple config and annotation', async () => { @@ -218,9 +222,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toStrictEqual({ baseUrl: 'https://jenkins.example.com', crumbIssuer: undefined, @@ -257,9 +262,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -296,9 +302,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -335,9 +342,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', @@ -363,9 +371,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -391,9 +400,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -424,9 +434,10 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith(entityRef, { - backstageToken: undefined, - }); + expect(mockCatalog.getEntityByRef).toHaveBeenCalledWith( + entityRef, + undefined, + ); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index c4b4ef58dd..0feb91d68e 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { createLegacyAuthAdapters } from '@backstage/backend-common'; +import { + AuthService, + BackstageCredentials, + DiscoveryService, +} from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, @@ -34,7 +40,7 @@ export interface JenkinsInfoProvider { */ jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise; } @@ -183,27 +189,37 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { private constructor( private readonly config: JenkinsConfig, private readonly catalog: CatalogApi, + private readonly auth: AuthService, ) {} static fromConfig(options: { config: Config; catalog: CatalogApi; + discovery: DiscoveryService; + auth?: AuthService; }): DefaultJenkinsInfoProvider { + const { auth } = createLegacyAuthAdapters(options); return new DefaultJenkinsInfoProvider( JenkinsConfig.fromConfig(options.config), options.catalog, + auth, ); } async getInstance(opt: { entityRef: CompoundEntityRef; jobFullName?: string; - backstageToken?: string; + credentials?: BackstageCredentials; }): Promise { // load entity - const entity = await this.catalog.getEntityByRef(opt.entityRef, { - token: opt.backstageToken, - }); + const entity = await this.catalog.getEntityByRef( + opt.entityRef, + opt.credentials && + (await this.auth.getPluginRequestToken({ + onBehalfOf: opt.credentials, + targetPluginId: 'catalog', + })), + ); if (!entity) { throw new Error( `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 06a04d8266..bda9686efb 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; @@ -25,17 +28,24 @@ import { PermissionEvaluator, toPermissionEvaluator, } from '@backstage/plugin-permission-common'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { stringifyError } from '@backstage/errors'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import { jenkinsPermissions } from '@backstage/plugin-jenkins-common'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; /** @public */ export interface RouterOptions { logger: Logger; jenkinsInfoProvider: JenkinsInfoProvider; permissions?: PermissionEvaluator | PermissionAuthorizer; + discovery: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @public */ @@ -56,6 +66,8 @@ export async function createRouter( : undefined; } + const { httpAuth } = createLegacyAuthAdapters(options); + const jenkinsApi = new JenkinsApiImpl(permissionEvaluator); const router = Router(); @@ -70,9 +82,6 @@ export async function createRouter( '/v1/entity/:namespace/:kind/:name/projects', async (request, response) => { const { namespace, kind, name } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const branch = request.query.branch; let branches: string[] | undefined; @@ -96,7 +105,7 @@ export async function createRouter( namespace, name, }, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); try { @@ -123,9 +132,6 @@ export async function createRouter( router.get( '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber', async (request, response) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const { namespace, kind, name, jobFullName, buildNumber } = request.params; @@ -136,7 +142,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const build = await jenkinsApi.getBuild( @@ -154,9 +160,6 @@ export async function createRouter( router.get( '/v1/entity/:namespace/:kind/:name/job/:jobFullName', async (request, response) => { - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const { namespace, kind, name, jobFullName } = request.params; const jenkinsInfo = await jenkinsInfoProvider.getInstance({ @@ -166,7 +169,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const build = await jenkinsApi.getJobBuilds(jenkinsInfo, jobFullName); @@ -182,9 +185,6 @@ export async function createRouter( async (request, response) => { const { namespace, kind, name, jobFullName, buildNumber } = request.params; - const token = getBearerTokenFromAuthorizationHeader( - request.header('authorization'), - ); const jenkinsInfo = await jenkinsInfoProvider.getInstance({ entityRef: { kind, @@ -192,7 +192,7 @@ export async function createRouter( name, }, jobFullName, - backstageToken: token, + credentials: await httpAuth.credentials(request), }); const resourceRef = stringifyEntityRef({ kind, namespace, name }); @@ -202,7 +202,7 @@ export async function createRouter( parseInt(buildNumber, 10), resourceRef, { - token, + credentials: await httpAuth.credentials(request), }, ); response.json({}).status(status); diff --git a/plugins/jenkins-backend/src/service/standaloneServer.ts b/plugins/jenkins-backend/src/service/standaloneServer.ts index f89f235667..ab77bb6573 100644 --- a/plugins/jenkins-backend/src/service/standaloneServer.ts +++ b/plugins/jenkins-backend/src/service/standaloneServer.ts @@ -14,17 +14,19 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { HostDiscovery, createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { JenkinsInfo } from './jenkinsInfoProvider'; +import { Config } from '@backstage/config'; export interface ServerOptions { port: number; enableCors: boolean; logger: Logger; + config: Config; } export async function startStandaloneServer( @@ -41,6 +43,7 @@ export async function startStandaloneServer( return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' }; }, }, + discovery: HostDiscovery.fromConfig(options.config), }); let service = createServiceBuilder(module) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..675020750d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7058,6 +7058,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" From 5b2452dcda52c1157dbd7589dc25c3b627f3fd9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 00:47:24 +0000 Subject: [PATCH 313/483] fix(deps): update dependency @uiw/react-codemirror to v4.21.24 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..9bd89355ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20086,9 +20086,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.21.23": - version: 4.21.23 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.23" +"@uiw/codemirror-extensions-basic-setup@npm:4.21.24": + version: 4.21.24 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.21.24" dependencies: "@codemirror/autocomplete": ^6.0.0 "@codemirror/commands": ^6.0.0 @@ -20105,19 +20105,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: cd17481d9d9a9b620f961a4df6e8208bcabe98652ca6c18366a8688fbcc09d37a030694a70dcc010aaabe02c85c342acf9f80357c6853b57f1081af5b130bd26 + checksum: db42a1651d7d482e1811cd629a3a8a53c3ac09bfabf376dd35c0cbbaf5780f80c873d6da55294245850f7d8e13da92ec5885eea4938c85c957c38c23394e11f9 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.21.23 - resolution: "@uiw/react-codemirror@npm:4.21.23" + version: 4.21.24 + resolution: "@uiw/react-codemirror@npm:4.21.24" dependencies: "@babel/runtime": ^7.18.6 "@codemirror/commands": ^6.1.0 "@codemirror/state": ^6.1.1 "@codemirror/theme-one-dark": ^6.0.0 - "@uiw/codemirror-extensions-basic-setup": 4.21.23 + "@uiw/codemirror-extensions-basic-setup": 4.21.24 codemirror: ^6.0.0 peerDependencies: "@babel/runtime": ">=7.11.0" @@ -20127,7 +20127,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 7d0209d947e1e57cf80ef44a097bb3af04d2bab2d6fc57deed91619a0db77e3d4289db8f03fb43906b2e324496d25c872fa99b83834f085c40ad95137ea4459c + checksum: 6adbee6608f3ec0806ff4b2d2e759fd314d3727c805cf58a6b1d524fe4e12b261b38d31374307eeb51aa34f68e113b298efdc13de807e93ca50a357e5b0f3eff languageName: node linkType: hard From c69abca233b5535f070bee2491c038085bced935 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 00:48:46 +0000 Subject: [PATCH 314/483] fix(deps): update aws-sdk-js-v3 monorepo to v3.521.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 1518 ++++++++++++++++++++++++++--------------------------- 1 file changed, 759 insertions(+), 759 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..43b561f068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -363,569 +363,569 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.515.0" +"@aws-sdk/client-cognito-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: e254357719b355a7c6cdd718c3896aca9971ea9479887c221841ec2c6e91f15b880f6a5d1ebfc62af223b4d02a0d4a882aa8394b436f9b544658ee87a481e1ac + checksum: e664da040d7688b97603dc49310c40acaae95c87dd2b32786664d5ee553a5e4079677b5ebcf5e85137434eafebe20c1d9f6f49b05926394d5b82cac2bcc2bbb8 languageName: node linkType: hard "@aws-sdk/client-eks@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-eks@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-eks@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 - "@smithy/util-waiter": ^2.1.1 + "@smithy/util-waiter": ^2.1.2 tslib: ^2.5.0 uuid: ^9.0.1 - checksum: 38366b504f5cda083637801b8a89cb03fb05fca56e6266a18d4fa4772a5556a461efcb82c8ed9690d8ba4c49e2ec2bdd71c7d3f987d2e7a85506439de2199ff6 + checksum: c31d933e4f64173276e6c3a9cba1e699fc25d8b43daff15cc52e23c4bbfdbac63f7e6cc8c55910bde551b568c6c7f928b432d518fe6251c5d1fad65854d44861 languageName: node linkType: hard "@aws-sdk/client-organizations@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-organizations@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-organizations@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 4b947f1fc5ca196b007855cc5d75accb82825adfe4dea370d181b9605bfe827ddc0f6cb073d7ef7f768a523be4e393a2dc1e28a999d118bda050f354997e8f60 + checksum: 1191e702f2123f2316f175dc81e3c00df05c79a53be709f166834c8425a600cff4d61bb0db7befc1fa3fc801dcccc05faab87f74adf0100b8428b4ee26c0a963 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-s3@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-s3@npm:3.521.0" dependencies: "@aws-crypto/sha1-browser": 3.0.0 "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-bucket-endpoint": 3.515.0 - "@aws-sdk/middleware-expect-continue": 3.515.0 - "@aws-sdk/middleware-flexible-checksums": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-location-constraint": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-sdk-s3": 3.515.0 - "@aws-sdk/middleware-signing": 3.515.0 - "@aws-sdk/middleware-ssec": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/signature-v4-multi-region": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@aws-sdk/xml-builder": 3.496.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/eventstream-serde-browser": ^2.1.1 - "@smithy/eventstream-serde-config-resolver": ^2.1.1 - "@smithy/eventstream-serde-node": ^2.1.1 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-blob-browser": ^2.1.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/hash-stream-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/md5-js": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-bucket-endpoint": 3.521.0 + "@aws-sdk/middleware-expect-continue": 3.521.0 + "@aws-sdk/middleware-flexible-checksums": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-location-constraint": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-sdk-s3": 3.521.0 + "@aws-sdk/middleware-signing": 3.521.0 + "@aws-sdk/middleware-ssec": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/signature-v4-multi-region": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@aws-sdk/xml-builder": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/eventstream-serde-browser": ^2.1.2 + "@smithy/eventstream-serde-config-resolver": ^2.1.2 + "@smithy/eventstream-serde-node": ^2.1.2 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-blob-browser": ^2.1.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/hash-stream-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/md5-js": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-retry": ^2.1.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-retry": ^2.1.2 + "@smithy/util-stream": ^2.1.2 "@smithy/util-utf8": ^2.1.1 - "@smithy/util-waiter": ^2.1.1 + "@smithy/util-waiter": ^2.1.2 fast-xml-parser: 4.2.5 tslib: ^2.5.0 - checksum: f61f91fb45500108520357e61a018975f4e4a49df378fa16ad0fc15b59560bdfcc11c6cb7d95ad1c4b6e0607e99788662e4013d8d529af75a36dffa5c8bfe5ca + checksum: bf3c5d6a42df6812f5645751bdbbb6cd69f8b28c3f0dd5ba7697a631e7573494e7cdc2bdc8081e49e5159a73c5a32259575a9d79f170753e8fbc53613e3ecc9c languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sqs@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/client-sqs@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-sdk-sqs": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/md5-js": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-sdk-sqs": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/md5-js": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: e92bbea7c7453cee74898ffde1092437693eb19b3a623243913805229ab6b1ec7905735a2dd58476915d574beeca2675f00eb89b5de8c3f099becc68c9ade901 + checksum: 9f65ffa57f0da1279f00754234e504b9f2250085a2bc843605f591b54a67929b73ee4402a40a291a5f9ce9e9936ad1d31676e37a12bb30c4c24f2bcb17483c5b languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.515.0" +"@aws-sdk/client-sso-oidc@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.515.0 - checksum: f220a9ba8542460b2aa91ad060302fb9e68bdf096ecca2ec1d6e525f4df1036b330cb85d20bac3e8399276c0d8d8d388b3f2191b58804919c68369afac0be37b + "@aws-sdk/credential-provider-node": ^3.521.0 + checksum: da6b724cd91f128192eba0bbf0827c7e6fccb30f899240eb908eb62d0a57437e1ac7e28097d31b8af1d616a35a37af97f8dcf9137230cb98e1b6cc33a9f38d36 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sso@npm:3.515.0" +"@aws-sdk/client-sso@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sso@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 12287dfa469fb2c6b5bedd3cbd37f7416f8234669b5ed0ff38cb1217d746ba6a5e6ff227b091a7751d1c20489a6b8bd93bcbed8f394cb4c51b6bebb9a9f79108 + checksum: 1035c3beb9d090d6f3858be022c66127d246e2b6c88336c808300814628acd18666cb76f6f6dfcf622b3b67dd195a29230602608bea9b40dde54b98079331359 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.515.0, @aws-sdk/client-sts@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/client-sts@npm:3.515.0" +"@aws-sdk/client-sts@npm:3.521.0, @aws-sdk/client-sts@npm:^3.350.0": + version: 3.521.0 + resolution: "@aws-sdk/client-sts@npm:3.521.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.513.0 - "@aws-sdk/middleware-host-header": 3.515.0 - "@aws-sdk/middleware-logger": 3.515.0 - "@aws-sdk/middleware-recursion-detection": 3.515.0 - "@aws-sdk/middleware-user-agent": 3.515.0 - "@aws-sdk/region-config-resolver": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@aws-sdk/util-user-agent-browser": 3.515.0 - "@aws-sdk/util-user-agent-node": 3.515.0 - "@smithy/config-resolver": ^2.1.1 - "@smithy/core": ^1.3.2 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/hash-node": ^2.1.1 - "@smithy/invalid-dependency": ^2.1.1 - "@smithy/middleware-content-length": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@aws-sdk/core": 3.521.0 + "@aws-sdk/middleware-host-header": 3.521.0 + "@aws-sdk/middleware-logger": 3.521.0 + "@aws-sdk/middleware-recursion-detection": 3.521.0 + "@aws-sdk/middleware-user-agent": 3.521.0 + "@aws-sdk/region-config-resolver": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@aws-sdk/util-user-agent-browser": 3.521.0 + "@aws-sdk/util-user-agent-node": 3.521.0 + "@smithy/config-resolver": ^2.1.2 + "@smithy/core": ^1.3.3 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/hash-node": ^2.1.2 + "@smithy/invalid-dependency": ^2.1.2 + "@smithy/middleware-content-length": ^2.1.2 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 "@smithy/util-base64": ^2.1.1 "@smithy/util-body-length-browser": ^2.1.1 "@smithy/util-body-length-node": ^2.2.1 - "@smithy/util-defaults-mode-browser": ^2.1.1 - "@smithy/util-defaults-mode-node": ^2.2.0 - "@smithy/util-endpoints": ^1.1.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/util-defaults-mode-browser": ^2.1.2 + "@smithy/util-defaults-mode-node": ^2.2.1 + "@smithy/util-endpoints": ^1.1.2 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 "@smithy/util-utf8": ^2.1.1 fast-xml-parser: 4.2.5 tslib: ^2.5.0 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.515.0 - checksum: 9af6a2484909e88a83c411551d55ad149c80a8f449c2e54c499769535243602a6283cd71f6a0cf975b295a321a74e90eb95f6659bba93bae3d12e2186e7545f4 + "@aws-sdk/credential-provider-node": ^3.521.0 + checksum: 1ca480532746fa6d81bf84bebf6e38ab3d2565789654465ec22bd8c34daf2c64165a90d70e29da8170da7ba9a18041c3da4a037f37689939798bf39c03348a50 languageName: node linkType: hard -"@aws-sdk/core@npm:3.513.0": - version: 3.513.0 - resolution: "@aws-sdk/core@npm:3.513.0" +"@aws-sdk/core@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/core@npm:3.521.0" dependencies: - "@smithy/core": ^1.3.2 - "@smithy/protocol-http": ^3.1.1 + "@smithy/core": ^1.3.3 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 94a41263e5d0c754f4d6d603572704822b570d5fc5ed450c8eb461b989198b625d2c115a470b087defe2c6c45b9442527062382c9bb1ca32842332317300b2fe + checksum: 43d02d64563b6fc5c55be1fd62dc67a95b862e8355d9c8d574c09fdc3668f7524723aec7a23ce29a40188d56479cba705dd8470787bebfd30b98c046f3b29606 languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.515.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.521.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-cognito-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: cb8cb5fa19b3e10e0a816eddda0a56a412da6bc68eba4807698ed94ef6bf2f3de288d959ced099b399dfc29f08154c84f8c025816e94226e5fb21c605e710542 + checksum: fce4a6b934839cb347fa603eb2c50f70e96e939528d3747492658de98e27d4f412256a40d3b63bb8d983ba81b695c7fff718632bd311c3d74e14194ea4553d95 languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.515.0" +"@aws-sdk/credential-provider-env@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 3573bc3f1aa89bc8eedb9eb39c8c1d501a68aec5eb059364a1091c8bf10dfda9cfbd78ee49d3ad25ec1012f765a4464363c4cd70997e94005fd21245871a4229 + checksum: 5b217fa1fc86f1d553bab39ac30942e06dddbffe3061cfcafb978f61740749eec9155271ba241df8889c49e73b498a2e4036b384b8b6de8c63da8f5682228d41 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.515.0" +"@aws-sdk/credential-provider-http@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/node-http-handler": ^2.3.1 + "@aws-sdk/types": 3.521.0 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/node-http-handler": ^2.4.0 "@smithy/property-provider": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-stream": ^2.1.2 tslib: ^2.5.0 - checksum: d13943dc7a83c9c129dd03a8b337b7753c791441d65a894085e00146d807738b420b9127f570a32497665be4f6e1cc8eeefd7cb1b013a04789d874c8b23b829f + checksum: b953861a460c2c871a390036e6ed5acdfd545e23204c56d2ae7835c46a43a598103175b9071107f9e817b9c386edc98a3b5e677ecc42b313b2d48cea2d11ca8e languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.515.0" +"@aws-sdk/credential-provider-ini@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.521.0" dependencies: - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c136d4257460be8331d7645854b7e3c91205a1eb12efd3dcbcd84501c787085292d1ccc4578c4776308d91748bde5af2c6eaa873b56aed83ab472a878a2d8883 + checksum: 41c9eda9ec49927999aab9137c46392576b84611c434c369c004eb99fcad212188dcf8dbc3bb9388a65b0077b52835b5219c0f68cfd640c2119772678608fad1 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.515.0, @aws-sdk/credential-provider-node@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.515.0" +"@aws-sdk/credential-provider-node@npm:3.521.0, @aws-sdk/credential-provider-node@npm:^3.350.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.521.0" dependencies: - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-http": 3.515.0 - "@aws-sdk/credential-provider-ini": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-http": 3.521.0 + "@aws-sdk/credential-provider-ini": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c51f267c61de0d82afe47d97cdf58971e3b8eec6e7364fe28d3867addd65e156358b96c39a335fad521e9a6925b349a967ae3eed1aa6e9cb4bcc1f0931e3ed50 + checksum: eaa75f81151113f84ccd7e26964a139fd598645f93074431ec5979a5ebf926df1da8659ecb46b4d79e4b83de30f7136954ea72fadc60a4e8bc5e828f193b7556 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.515.0" +"@aws-sdk/credential-provider-process@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 11159b4c9502218ec6cba9a46ddc120e53aec7f04507c14d4e99a186073cfd363af438c623705890a2e8f6cc792475ebd14c82766dad82dabf7f20d9708f7faf + checksum: 7770461063e9c330331f48401a16d0f792c97c5392b2f393e09f1be6c5ec0f97316c7fa11c2ebfc7392984acced03d5205558b264ed6417697993a9a466185b2 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.515.0" +"@aws-sdk/credential-provider-sso@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.521.0" dependencies: - "@aws-sdk/client-sso": 3.515.0 - "@aws-sdk/token-providers": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sso": 3.521.0 + "@aws-sdk/token-providers": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: fbe1eebc50e9bd3715bca4e6ee1a2922cf1ea383953730a6bd3b88b12f23a95cb3ff0edaf8578c81156ef65648eb0295f5c39ed6ae2c8e16b6ce9d4c46f207e9 + checksum: 76fbed5935eb4f7cd244f9d90bb5b9f1069226ecaaf03d69f16b4f607864541cc6b8e6d038a07eef27acff75df8f539c3c61d6b672f5bda4db1a1a3966815566 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.515.0" +"@aws-sdk/credential-provider-web-identity@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.521.0" dependencies: - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f0a7e9855f78849143c3139c613cc1531a88272f3ec039d23ac9366b94495a3e07212ade9541e56c93560ad9f58600376e9de38706a4cd97aaf5f400911361cd + checksum: 4e9360d0e6a55b7f60d837addb87dbd5d804e4202c2fadc75d11196a54dbb6099fa977fb0a51191d8c62373f17279c379a304b697ddd15abe5508a30c2a34556 languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/credential-providers@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/credential-providers@npm:3.521.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.515.0 - "@aws-sdk/client-sso": 3.515.0 - "@aws-sdk/client-sts": 3.515.0 - "@aws-sdk/credential-provider-cognito-identity": 3.515.0 - "@aws-sdk/credential-provider-env": 3.515.0 - "@aws-sdk/credential-provider-http": 3.515.0 - "@aws-sdk/credential-provider-ini": 3.515.0 - "@aws-sdk/credential-provider-node": 3.515.0 - "@aws-sdk/credential-provider-process": 3.515.0 - "@aws-sdk/credential-provider-sso": 3.515.0 - "@aws-sdk/credential-provider-web-identity": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-cognito-identity": 3.521.0 + "@aws-sdk/client-sso": 3.521.0 + "@aws-sdk/client-sts": 3.521.0 + "@aws-sdk/credential-provider-cognito-identity": 3.521.0 + "@aws-sdk/credential-provider-env": 3.521.0 + "@aws-sdk/credential-provider-http": 3.521.0 + "@aws-sdk/credential-provider-ini": 3.521.0 + "@aws-sdk/credential-provider-node": 3.521.0 + "@aws-sdk/credential-provider-process": 3.521.0 + "@aws-sdk/credential-provider-sso": 3.521.0 + "@aws-sdk/credential-provider-web-identity": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/credential-provider-imds": ^2.2.1 "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 8890e83fc19072c51cee8d50ee7168800b9775aa00bcb7a0feb836e60fbae0fb8ba55133d256719033cd7ccda0f5881350474ba3f4769016f01ce7de4ef7b6e5 + checksum: c4b3ca40a4e7a9843847f81f0a3f3ac568986e87054e44df8cad8ddf3f8df6314c4e67ca27b150b05cdfe6e96ccca32457654a121b09a9ba017819b9000d534b languageName: node linkType: hard @@ -951,34 +951,34 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.350.0": - version: 3.515.0 - resolution: "@aws-sdk/lib-storage@npm:3.515.0" + version: 3.521.0 + resolution: "@aws-sdk/lib-storage@npm:3.521.0" dependencies: "@smithy/abort-controller": ^2.1.1 - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/smithy-client": ^2.3.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/smithy-client": ^2.4.0 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: ^2.5.0 peerDependencies: "@aws-sdk/client-s3": ^3.0.0 - checksum: b4d7b3783508ce3ef93150b2a249490c5af803d02d91ce81aebf0ec055870aae3260d4d66e9bf1d4234fb61d11cfaa54f2916c9c2572cf4cb6ee1b15993c41b5 + checksum: b55cdbe2744970f863f30e84ba39dc25d123032cf5e7b11a3fb8d6dc0aff7f3311f00112c72b686f4833ff6494c3b1c346cdb03cc5727652933ebd98dc1f8cf7 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.515.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@aws-sdk/util-arn-parser": 3.495.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: 8ecec09ac50c33a24178e73be6b4238e5047984eebf54c4786392e82d2b1cdd09807ee97104f603d22890a58657335843f9587c66e544c2ffbb9a25f3e3bf065 + checksum: 407ea1c7d64159fa86ccc002333c646b841fa7cbec70400798af3b688418a1f44bea31dac4e462383f9221144b113c9ee33c5e585e1ef9d7d207cb91a2439cc7 languageName: node linkType: hard @@ -995,108 +995,108 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.515.0" +"@aws-sdk/middleware-expect-continue@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c989e0e55f51c631f914b444a0e3a6a81c79c1ad8046346c16a4ccfbb77aeaf48ba948a6b8185fc36c00abf07951cc374c3673da6d23d7de6cd3ec8d5b4d840e + checksum: 28ed930f9e4d9d90d705c371c2ac3055fa1fdb8c6de44d1db899f6ade1231cbe3de7d4f6e7fa7dae41fe0d79d5ee5b068ba8ecaa885dc6a2a6100cc230b0847a languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.515.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.521.0" dependencies: "@aws-crypto/crc32": 3.0.0 "@aws-crypto/crc32c": 3.0.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/is-array-buffer": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 89f2df7a3dd40c174586aec1349e4e144825cffb252df843b115390702523dc6cfb745756a69a1ab1e77e44340fd55bed04be3856fe96d937af5702bcbcf869b + checksum: 60a77546090174ca7cfd8d6894c6d4583ae5ad311f85dc407c1d98128fa16322850a7284a4f75260d4ab5529296110b0952997c1726c325813988c14e2aba792 languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.515.0" +"@aws-sdk/middleware-host-header@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ff066cf47b0ba2c64bd70efdec795ac2da8bad7ba8dd44913c98f42b153ca6e753b13b6c1ef7075499590279a5cc49b5a60511dae4512dcdb11a62a0e67fa061 + checksum: e0e0597f436bce61c9fc2598d65db68610d8a4a8576e2d9073d8ffa0fe3099a76570c997fc9ab46ef18b1530102ef73101db04d28ce0a40f40ca76bf502d8db9 languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.515.0" +"@aws-sdk/middleware-location-constraint@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c3e7c7b51c276eba7ceef05c18416d2cca976c83ac9db227877090c33f676b50b0c90ceaf5d924cd9894a4c2f44c0a2d06ec93da753ac646fecb68d5facef570 + checksum: 62b8ac417945c826a3042369116250e17b31df2f7f949494320de695985db0801986968503b366965dd0e1beaf25bd37890261dbc2e5fdbf2b0f3cf581ace603 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-logger@npm:3.515.0" +"@aws-sdk/middleware-logger@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-logger@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 32d251e77f43593ffdd192a4d0628f33773e29c14a3001a4c6519553e94958edbd0fb8e6954a65d1180b0caa16cafe9fc9b362d1ab663db1d1eac84b15667645 + checksum: 9ff749309bd457be1356d3efea53d9067c15baa631d4ba7d874172f087030d4ccbb7df38ccdecb6823944af2732b01caa3cd495a245fa5a4975a5442e2059532 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.515.0" +"@aws-sdk/middleware-recursion-detection@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 23c4a1e4d7de86196acfcfbc84bea84c8c3211c4831fdc7c975a6388022037bd5baa4e5809dca188631f06726b51fcf85af358f9ef526e255dc494b368d0da0c + checksum: a097d83c411944d30105a997520791d9a16800a4d5b5b8a77ef5dd8edb3616ecf358a669a4c116314bf60d2ab1c23d54f0c1d79a2e2bcfa38b84a5ec3b418b86 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.515.0" +"@aws-sdk/middleware-sdk-s3@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@aws-sdk/util-arn-parser": 3.495.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 tslib: ^2.5.0 - checksum: cb67334b30eee8fcf52637407cab2787353e463bdf5a99a33966f0e765f6f8fc687c4bba2b63ec7a4374b2ab544be807e1849bd910e6f163ff43d036f026e7e1 + checksum: f29d1eed5f3f4de2bb0e85bd572d31f41f494f83f3903edaa45bb79024b6e7cca4bc404a57bb2d69dad58ea1ab92942016a7cd9a5295a54e96851e44a9ce649f languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.515.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-hex-encoding": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 67a7d9ed3e975a3fd83266f18ce2d94e3dc457d7001af7031e732b2d96135678dd3323ecae56ffe214f3dbe23d1df295a0cd0d21df7f6b9c98699e6a9b4704ca + checksum: 4fcb19c74ce0a667b78fb053b25a4c6c57d84f8eb2167045922a6310d7ecc9def78abe6f2f310ac78327dc41f54347df0497098db00b571cf26029bee7eff1f7 languageName: node linkType: hard @@ -1110,42 +1110,42 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-signing@npm:3.515.0" +"@aws-sdk/middleware-signing@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-signing@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 7ee85c70a81b85e455c4411caad79c7b41a0a0fd9696feead394a31ca360b096c74f7b2b6b5d449fcfa622659305618e24a49d15c45cab3afa54503459a9b24a + checksum: 545225d39d0e6133f14ca6ac721b78293b7ae2b522d9229f3c4afa00efe5ccd38c2b11a508bbec050f2b91a68f51fe6260db57853e8c28f7f80d09b93ef6120f languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.515.0" +"@aws-sdk/middleware-ssec@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 3a91ebaf128ff63665f9aa69b9b2d475ebb8e519da2e94a142bcea7f9b173be52a0476b814e90e88e0936db3371e3c1c090b4e44018f006e1cfc37d2631a36d8 + checksum: 86b31dcc825d194898dbcc72e9a7590e9693bfbe8d3e0707f7b82814f735dfb73c06f77d68785b061faf173b8a1da6f47f98564602437532c75f8da77f820aca languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.515.0" +"@aws-sdk/middleware-user-agent@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@aws-sdk/util-endpoints": 3.515.0 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@aws-sdk/util-endpoints": 3.521.0 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: fd601cb0367d42e38b71494c773d82bde8970f9aafbdbf18d7cadc25732ecc2b787f0cfef2110755d0cef73d6aa3ce2ca77dc5353854bedaf392a69019b39ac2 + checksum: 5d1461de4d6d6c7cfd6f7bca9753f3c8340b3bb27638940000d14811ff9a3e63ffecc9c9e4b8784e53a834815d58f902ae9e4a07b60b62d56629359de80fbe6c languageName: node linkType: hard @@ -1193,31 +1193,31 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.515.0" +"@aws-sdk/region-config-resolver@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 0ed7fbd6390baebdf511b30877236fa8be8716e0162e2c9e0138c9b41ebda7d99a6f3d6cf66cb4af24761631c2c29102ecfe7a5f08894e1de3c98ca6a135fa74 + checksum: ce0ec289d6ca59747c1e96dd3b45f11fe690fc2b0407beacadfd07e04258474c2be51a265851ef7bca0feb5f0e7ba6520ee2c5de2b112dff1fbd5c37901e2e72 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.515.0" +"@aws-sdk/signature-v4-multi-region@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.521.0" dependencies: - "@aws-sdk/middleware-sdk-s3": 3.515.0 - "@aws-sdk/types": 3.515.0 - "@smithy/protocol-http": ^3.1.1 + "@aws-sdk/middleware-sdk-s3": 3.521.0 + "@aws-sdk/types": 3.521.0 + "@smithy/protocol-http": ^3.2.0 "@smithy/signature-v4": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 1f780409af431b3ac91beee294cf4af74fff9f3dffc21ddd1c8a1782a01916da162ceec27c133a9e86452279bdf0005f9118809820d557a248613cd44f0f7ec3 + checksum: 312334438f12927e842d0a5c1d3fd837bedb54bc96caa7a7c0f1168a87532ee1b2e90090a2e1369c28cc0dc4b24c79ae7bf9aca66cbb811b61c8fdda775c4581 languageName: node linkType: hard @@ -1237,17 +1237,17 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/token-providers@npm:3.515.0" +"@aws-sdk/token-providers@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/token-providers@npm:3.521.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.515.0 - "@aws-sdk/types": 3.515.0 + "@aws-sdk/client-sso-oidc": 3.521.0 + "@aws-sdk/types": 3.521.0 "@smithy/property-provider": ^2.1.1 "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ab51c440da9772d0ee58948241b975705c171395cf1bad81a4ffd8f11f34106af54dcba930e360fb19489beedc1106ac14432bd27abdfe4b7536dc2113841027 + checksum: e34671eaab24dac569d0a98a87b21e2a6ff0960cdc931276f446a31310bff343af33dcf742738a02c9eb8e01226981ab64abd8e59e7758ed695f0045bb524af6 languageName: node linkType: hard @@ -1261,13 +1261,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.515.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": - version: 3.515.0 - resolution: "@aws-sdk/types@npm:3.515.0" +"@aws-sdk/types@npm:3.521.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": + version: 3.521.0 + resolution: "@aws-sdk/types@npm:3.521.0" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 0874f1814b58eae6e7115c3d08c2bc56e558e73d1ff8c5f833a73b4a0f76a42743c83c36a4b2759177e41b1feff065e85450f7bc235a087b94e67db12f87d298 + checksum: 28d9ab39ad19e74ca721100131152bec975cea3c78e5013e70e9684b051c5115623430a923f0e92426b298033be94ebd554925ec4a5fb64273c48df90ea6c6eb languageName: node linkType: hard @@ -1301,15 +1301,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-endpoints@npm:3.515.0" +"@aws-sdk/util-endpoints@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-endpoints@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 - "@smithy/util-endpoints": ^1.1.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 + "@smithy/util-endpoints": ^1.1.2 tslib: ^2.5.0 - checksum: 1ab8fcd3054dc0366f10813a01130d05f4ba33f1488c1a168f44881cb24f3fbc2393111b7b0fd4dc06c852e4c9a5bbe8a82717b72229b0977cdba8a631ddeee1 + checksum: a8f01159d4114a7893200a3a782ccec091da7a46b4de5bb1c4db253ad836de123fbb314c376798044119b49f21b9d3bfd61eecb4328da9a73279719079baba48 languageName: node linkType: hard @@ -1361,32 +1361,32 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.515.0" +"@aws-sdk/util-user-agent-browser@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/types": ^2.10.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 40f518006cb7e76d06d83dcf05222b0b0ff47c10b63149cd5db2c0c1db79c8eff34bd582e89c748897bc11697b7b357bdca77d569f57ad0b2081c088752d601f + checksum: 1938f4e00873a3d0ba55988d562fe987352abf6c57fef7c27dc28f561a7752af73d34883eb89eba1173b42b8d31ed0d0a4fc2592b46c28e775545bdb27b0bc80 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.515.0": - version: 3.515.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.515.0" +"@aws-sdk/util-user-agent-node@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.521.0" dependencies: - "@aws-sdk/types": 3.515.0 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@aws-sdk/types": 3.521.0 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: 4e91d9cd5bbe4aa8321417ea1bd9caf3229416ee624b7f67b5206b284a539116692412ca41d60dcb5759b841fed7d9fb570915566d1f7e620578657f89548a23 + checksum: d78a47e32fef990da97635af88484649636bf452320ce80ed6376302d18014f7b191f5da3c096e44aa4f9054639c31c3b2e62d06e7708f747ef7847bad019aa7 languageName: node linkType: hard @@ -1409,13 +1409,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.496.0": - version: 3.496.0 - resolution: "@aws-sdk/xml-builder@npm:3.496.0" +"@aws-sdk/xml-builder@npm:3.521.0": + version: 3.521.0 + resolution: "@aws-sdk/xml-builder@npm:3.521.0" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 42d9d60c1c7f8a22f6a64ac36ba9d5ccff200ce963beebb142ad4708d2486fe29b61ecb37bbccd6f0019e25107aa2327e6d8550d30214b4895e7219ccb8661c8 + checksum: 33a86fdcf93029706725f829b9cea1846c8c51d0170410e19c75564256cb5825fb4c9e65ecbb7930c5ee14855bb63425b596fc2660978c5724197fee1b09c8f5 languageName: node linkType: hard @@ -15966,158 +15966,158 @@ __metadata: languageName: node linkType: hard -"@smithy/config-resolver@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/config-resolver@npm:2.1.1" +"@smithy/config-resolver@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/config-resolver@npm:2.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 "@smithy/util-config-provider": ^2.2.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 18c8af60cbc528887a82dc0eabaf0b398d868511dc6b10fa01f41c77ea9c2679ab2137feaee51aa9060dbc5c46fc33325a659f4bd54549c203f64e15dbacbc0a + checksum: 20ac9423e416bbb486d1bca247d7a37a2cbffe30c2e292b15c25e411c6cc5af438362ba2aa8e2218e93ef10c7d7fa04873646c4dd82bbcb4df6203efd1f1d3c9 languageName: node linkType: hard -"@smithy/core@npm:^1.3.2": - version: 1.3.2 - resolution: "@smithy/core@npm:1.3.2" +"@smithy/core@npm:^1.3.3": + version: 1.3.3 + resolution: "@smithy/core@npm:1.3.3" dependencies: - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-retry": ^2.1.1 - "@smithy/middleware-serde": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-retry": ^2.1.2 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 5c716b170aa8fb6485b7c98d2d59c44a7333566345727472fb9fabbe86473b33f090fa7a3e08de6ca10829a048c5f20bd238da7da471214789171c7e0a4460a9 + checksum: bb8a79f51517049064f1b5fb2233b9410d8c6dfeafa2ecda31b9d5326f05ffa4773c38ec72d90b380366e8a9b4c21b93231aaadce262c0d87989904950358cd3 languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^2.2.1": - version: 2.2.1 - resolution: "@smithy/credential-provider-imds@npm:2.2.1" +"@smithy/credential-provider-imds@npm:^2.2.1, @smithy/credential-provider-imds@npm:^2.2.2": + version: 2.2.2 + resolution: "@smithy/credential-provider-imds@npm:2.2.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/property-provider": ^2.1.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/property-provider": ^2.1.2 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 tslib: ^2.5.0 - checksum: a4e693719384440718728772ea2126be133bbc83fa7bfcefd236942ccb28d1390f1b32fe3262bf330ba4c8e600d01ac73a57110eb42462ec1eb6bbd51e2676a6 + checksum: 85cc9a6e2c52a8f47c0db3dd11c31ff550e5ddd0f6d1917169e22fbf242bb5a5c05bc426da5d04bce79cf0633d0c645e83ca02d42e243f3b40c2ace07aeb1b56 languageName: node linkType: hard -"@smithy/eventstream-codec@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-codec@npm:2.1.1" +"@smithy/eventstream-codec@npm:^2.1.1, @smithy/eventstream-codec@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-codec@npm:2.1.2" dependencies: "@aws-crypto/crc32": 3.0.0 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-hex-encoding": ^2.1.1 tslib: ^2.5.0 - checksum: 7e59028a69e669d1ca1a0fef788f9892a427fad32f33ded731cbfa3bde0163acbc1e7d207e0ce3eae2d3b53f48dce7a99ded092122cdf78e4f392cffd762bfe3 + checksum: ea455826916906a480c7abc517ceb043b578a95994842f802f71e409c45a5575a4f337c376c0c371055371bebafb81119a044cdb4c7de057304e6b1c0527d1d9 languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-browser@npm:2.1.1" +"@smithy/eventstream-serde-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-browser@npm:2.1.2" dependencies: - "@smithy/eventstream-serde-universal": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-serde-universal": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c909b620de25e9779653742012c665df8c76bf5193bb79054ef302bc3c08b0fa5620884a5965a3a6ebbb4f059da1b05221662a7a652aa979f4830f26c534be60 + checksum: 855118cd6ffc99a05d4950a01af23f727599e7fb127323d508b935617005e3d0e45cf1025fe7ba5079d9def82c73dc8d3e703c36f4b71288fd945c1433b41777 languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-config-resolver@npm:2.1.1" +"@smithy/eventstream-serde-config-resolver@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-config-resolver@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 14d4d1c638be460290eb05dec3a700d742f8ce77814c1c235fbd7cf248941a387595f1cd684b9acfc3e081a8d9e6dc2810f10c894b3e08f16f0c3adb130cb736 + checksum: 397d91a492948bced849c5a90422ff6a2d49d4d300ec9fff09eea7256931402966d0285e8fd33a2df3745c10ff5dc88bbfafbf0ce53f0858371d02e7057fd033 languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-node@npm:2.1.1" +"@smithy/eventstream-serde-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-node@npm:2.1.2" dependencies: - "@smithy/eventstream-serde-universal": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-serde-universal": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 4be3dd11854d66310273bae07faafd4ca872158be8d3ef7bdc1dec55a175e983975750ebdaf762e74daf80495e379bd2791971a50899076865759a75b2634d73 + checksum: 56a65908d8ac07fd72dfa06a4709972c997a10696c0850de398804590fcc33144afbe1aa70b80ce7d98f6003dffa2b0f5d29228586fb2cb0f076b3d4c03ed20a languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/eventstream-serde-universal@npm:2.1.1" +"@smithy/eventstream-serde-universal@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/eventstream-serde-universal@npm:2.1.2" dependencies: - "@smithy/eventstream-codec": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/eventstream-codec": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 99c7cf5b869f8e6323e976335a3238b77d3b1c32005fc78093d448981883294e4d59bcbd419e88d6a53c76aab01c27bc9af63a5dfed9451d2302eaf6ccddbd64 + checksum: 693be21ef300c26f638fd7f9b9b36652aff319d7316893a661f32a5f1f29369bc216eb6f1d9c80d5e42473ccdd83e332163a8c9fce012c08df4305a52dea09c7 languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^2.4.1": - version: 2.4.1 - resolution: "@smithy/fetch-http-handler@npm:2.4.1" +"@smithy/fetch-http-handler@npm:^2.4.2": + version: 2.4.2 + resolution: "@smithy/fetch-http-handler@npm:2.4.2" dependencies: - "@smithy/protocol-http": ^3.1.1 - "@smithy/querystring-builder": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/querystring-builder": ^2.1.2 + "@smithy/types": ^2.10.0 "@smithy/util-base64": ^2.1.1 tslib: ^2.5.0 - checksum: c23701d45bca6842b5206939ccd587e3482ace9f656ae3dca92ff0bad3fefb846cc33683dff41a19186f2a5662ca6cd66c8aefda4664b7dfd95f9a616055a1c1 + checksum: 7d87d5c6674623250972ac673a3317eeaeeba0647d8095c92e63ec9a002e96bb56dd7aa75172e474e226a4971f2abbd2506025cb1bf131d3b45698dbff27220d languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-blob-browser@npm:2.1.1" +"@smithy/hash-blob-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-blob-browser@npm:2.1.2" dependencies: "@smithy/chunked-blob-reader": ^2.1.1 "@smithy/chunked-blob-reader-native": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f4dc57c11ef32ddea0e7094d2c230aa274f1e410d84c789d8f5e2ed8a090da8675ca76da9605d297285324107ea8106af1c2aab2859bd62d6e9a8db415eb8e55 + checksum: e8d9fcedcfd03d03603753d6e4aafd7ad7c26e9ed629bf54a8dbb2ecd14b9e29cd267209453479eab698cb19329bfc80632823b40f49cb6d8b949aa4cb6db7c4 languageName: node linkType: hard -"@smithy/hash-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-node@npm:2.1.1" +"@smithy/hash-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-node@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-buffer-from": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 5d5aae69b94dcb8abaf9f6a5b53ee320c9e126445c4540fcf2169e8ea7ebd953acff7fd77ba514614f6ebbb0baf412e878eebcc3427a5b9b6f8ee39abbc59230 + checksum: 2f4fe6120a177afbc540c0ba904a3285a0b81de576a57bb28dbee94186635ab585034e7f48eeff0950d3a6442f5fd932cd66c366199c42f4314a540d02261eba languageName: node linkType: hard -"@smithy/hash-stream-node@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/hash-stream-node@npm:2.1.1" +"@smithy/hash-stream-node@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/hash-stream-node@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: da3c4ba14c648ee0d2fe7d3298d601150ee0ce5ac0c7d9f54a88148b5f67b03513b41560f76f5f109f11196547b4dc4f26e314774794596d7e3ee1103a9906a8 + checksum: 5a1a4d4fff29a4ba048900c606c98baeac0daabbaeba77c0c5b603d5896eff7f1eba2b012d41344638ce3d7fe10cb7d8d88551d34f1c637e807adb61efe3f43e languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/invalid-dependency@npm:2.1.1" +"@smithy/invalid-dependency@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/invalid-dependency@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: f95ecd9acd337a408b6608a3f451b24a61e26149878f61fc7855c724888f0d28abf0b798d16990dadb7eafc8027098f934c0cd44e75d01d31617bd1fbfd93935 + checksum: 5f5ce3d408c67c3e8b80c7dbc3504662c437ded360c360f072ed731c0bae6b57386ba37dc059883dfe7d23711680a5119a68e05b3ea6ccc7fc124cf0e6f90024 languageName: node linkType: hard @@ -16130,93 +16130,93 @@ __metadata: languageName: node linkType: hard -"@smithy/md5-js@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/md5-js@npm:2.1.1" +"@smithy/md5-js@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/md5-js@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: d15bc426a46d80d450b555a5ccd3d5a6bf37190f4b9ccb705852cd53ce61e4fe6fb08a569b87303ee787da57023f2b75f0e7893644af16c89e9aaf513f8afff3 + checksum: c6e4bdb779e9af5146e502d1e0d757e09a991b70e39fdb089efbcb8e511761942330034b215b66814c3627b0b0bb7bd028d0f26e8558b0ec5b2ea07c46b5f4e5 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-content-length@npm:2.1.1" +"@smithy/middleware-content-length@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-content-length@npm:2.1.2" dependencies: - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: cb0ea801f72a1a01f5956b3526df930fc19762b07d43a3871ff29815f621603410753d37710d72675d9761b93da32a38cfd5195582de8b6a47e299b1f073be25 + checksum: ddea93b236e5f916da8e1574317967d5aa449e78b0c7153c60c821d117f1648b00effad5301095919de9225810cd8f90f8ee76e7b95c346fc616d8598ad54447 languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^2.4.1": - version: 2.4.1 - resolution: "@smithy/middleware-endpoint@npm:2.4.1" +"@smithy/middleware-endpoint@npm:^2.4.2": + version: 2.4.2 + resolution: "@smithy/middleware-endpoint@npm:2.4.2" dependencies: - "@smithy/middleware-serde": ^2.1.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/url-parser": ^2.1.1 - "@smithy/util-middleware": ^2.1.1 + "@smithy/middleware-serde": ^2.1.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/shared-ini-file-loader": ^2.3.2 + "@smithy/types": ^2.10.0 + "@smithy/url-parser": ^2.1.2 + "@smithy/util-middleware": ^2.1.2 tslib: ^2.5.0 - checksum: 685f74c76cba205bdb20ad7bda449b73e498ae2e9074a026d48b38c7b4456d8a0cfb4fdb48625b65f93f3a75e92eaf7951db28f8e9f44e50ce18fd59a7b325af + checksum: 3e989123fc608c9a32abf30c4033718b3da665a63bd84e8e2869d4aecb0545d461506e75f82b91bbc35a07915ddadf1432643e4c937c11447847a9a45b3de9fa languageName: node linkType: hard -"@smithy/middleware-retry@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-retry@npm:2.1.1" +"@smithy/middleware-retry@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-retry@npm:2.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/service-error-classification": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 - "@smithy/util-middleware": ^2.1.1 - "@smithy/util-retry": ^2.1.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/service-error-classification": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 + "@smithy/util-middleware": ^2.1.2 + "@smithy/util-retry": ^2.1.2 tslib: ^2.5.0 uuid: ^8.3.2 - checksum: a4bc59d2ff8f65367aeb93391a2aafc7caf8031d8b2dfb32ee35748cdc46e06d5182c37bee90d7a107e890959bd40e6a7f4041bc1b0b36a99d14919b1cc78812 + checksum: ec04fd0c362070529ecd52f2dadd6fd4d638a4e35c62a67309791f1a288550ee8fcd6406ec27fea78d3c89b08a9c6d29ce63bf7cb5a1b0269f86a759b233dac4 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-serde@npm:2.1.1" +"@smithy/middleware-serde@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-serde@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: ed77b80ac6b68640ee4bf8310bc4d9f5aa13de2741333f6f03a4983e897fa66e0de057d178e78d9ba095d5686d3e4531437c9dd2583366efe948bd75b2aa8581 + checksum: 4f5bd5ee173cf20cd1c12838b0802f96df5a14c3bdab2d50a6009965c128596863c095633958f0c74d82bca3cc9343f8a4c659c033b9a75e614e3a85e34e0665 languageName: node linkType: hard -"@smithy/middleware-stack@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/middleware-stack@npm:2.1.1" +"@smithy/middleware-stack@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/middleware-stack@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 0d7c1051c96fcf19f7d5e96bc59484ce13df4e570c1da3eda74d23a7911b41eb61d6c378aad0aa21f7e9c72934148bdf39f9767c57abd4845aa4417a84e3f6e4 + checksum: f93dda40f08051a6391e213cc3b90f30ebb31399b000bba882cbf37f942786030822e10ccfe576005361ef6341b571460ce791fbdc7dad971c99a33a92e400dd languageName: node linkType: hard -"@smithy/node-config-provider@npm:^2.2.1": - version: 2.2.1 - resolution: "@smithy/node-config-provider@npm:2.2.1" +"@smithy/node-config-provider@npm:^2.2.2": + version: 2.2.2 + resolution: "@smithy/node-config-provider@npm:2.2.2" dependencies: - "@smithy/property-provider": ^2.1.1 - "@smithy/shared-ini-file-loader": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/property-provider": ^2.1.2 + "@smithy/shared-ini-file-loader": ^2.3.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 62ed3124d888a10cac633a250fbe12d6c5b8aa75ea691889abebce227cbaf155f3db00fa6beb453fbd6147e667e70819d043da1750980669451281a28eafd285 + checksum: 666d80d893985e6af5aa88a2f3ed07bd68c6873805974331fd148ec5a5d331e5116e2dca8656ef57c60e22cec03aee0717061a03ce703a691765bf94d809f2eb languageName: node linkType: hard -"@smithy/node-http-handler@npm:^2.1.7, @smithy/node-http-handler@npm:^2.3.1": +"@smithy/node-http-handler@npm:^2.1.7, @smithy/node-http-handler@npm:^2.4.0": version: 2.4.0 resolution: "@smithy/node-http-handler@npm:2.4.0" dependencies: @@ -16229,17 +16229,17 @@ __metadata: languageName: node linkType: hard -"@smithy/property-provider@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/property-provider@npm:2.1.1" +"@smithy/property-provider@npm:^2.1.1, @smithy/property-provider@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/property-provider@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: e87d70c4efe07e830cfb2094b046af89175b87b13259fba37641aa7bfc2ab0c7bf2397797ac48b92e1feb11bf6129b82b350519172093efd7ac4d3a4a98bbe2f + checksum: df2b72912ede1843a75220a458e3ff8ec70e5544c990c0915e615507380cea4c28bb39b425b7ee600f5c3c90d53b5c82ceaf14d641348f465c14640c556ac9bd languageName: node linkType: hard -"@smithy/protocol-http@npm:^3.1.1, @smithy/protocol-http@npm:^3.2.0": +"@smithy/protocol-http@npm:^3.2.0": version: 3.2.0 resolution: "@smithy/protocol-http@npm:3.2.0" dependencies: @@ -16249,7 +16249,7 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-builder@npm:^2.1.1, @smithy/querystring-builder@npm:^2.1.2": +"@smithy/querystring-builder@npm:^2.1.2": version: 2.1.2 resolution: "@smithy/querystring-builder@npm:2.1.2" dependencies: @@ -16260,32 +16260,32 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-parser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/querystring-parser@npm:2.1.1" +"@smithy/querystring-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/querystring-parser@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: bfac40793b0e42f4e25137db4e7d866debfa32557359cc41e02a23174a6fd8e0132f098cef5669a3ddf5211e477c9c97d4aa9039b35c7b4a29f2207236da236e + checksum: 02a1e3a31b37b59adb162d3a2cb084852c2ea01dec948b0669939e77241b05fd7f5b00734418b925248f0b6c164bc483e897438cb2b1a750829f6b4aab0fa8d1 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/service-error-classification@npm:2.1.1" +"@smithy/service-error-classification@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/service-error-classification@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 - checksum: 59a5e3cb0fb42d70fc2d85814124abbff60e28cc9aa45d87fde3370e25943abaf4b6baf62cc40e496c3687e9fa9161156a055ad29a4f7ce8dd7d937bbf49f9a7 + "@smithy/types": ^2.10.0 + checksum: 8a26f553fd2a823179b701f87c0952e58580c9297166e608a036c7b313d5cc1399bc8b4b056b038003aedc0145643c6c6323e7f683b1a2d1140d9a7982e6bf7c languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/shared-ini-file-loader@npm:2.3.1" +"@smithy/shared-ini-file-loader@npm:^2.3.1, @smithy/shared-ini-file-loader@npm:^2.3.2": + version: 2.3.2 + resolution: "@smithy/shared-ini-file-loader@npm:2.3.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 89b0dfb65faab917fcb4a6a8f34a85d668a759ccbfd6c4dc3d6311e59a8f1b78baab1d97402c333d2207da810cb00de9d5b4379f114bde82135f9aa0d0069cab + checksum: 6db5ac83a76a15f3bf49496747ef4d20343e87a4de35b87892c5ac5c69a7046ffe7276230a4e9cbc075183d8b0584f1530a878f58324279cf936103c578aa70a languageName: node linkType: hard @@ -16305,17 +16305,17 @@ __metadata: languageName: node linkType: hard -"@smithy/smithy-client@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/smithy-client@npm:2.3.1" +"@smithy/smithy-client@npm:^2.4.0": + version: 2.4.0 + resolution: "@smithy/smithy-client@npm:2.4.0" dependencies: - "@smithy/middleware-endpoint": ^2.4.1 - "@smithy/middleware-stack": ^2.1.1 - "@smithy/protocol-http": ^3.1.1 - "@smithy/types": ^2.9.1 - "@smithy/util-stream": ^2.1.1 + "@smithy/middleware-endpoint": ^2.4.2 + "@smithy/middleware-stack": ^2.1.2 + "@smithy/protocol-http": ^3.2.0 + "@smithy/types": ^2.10.0 + "@smithy/util-stream": ^2.1.2 tslib: ^2.5.0 - checksum: 9b13c361528b3120b1a1db17cd60521d04c72f664c2709be20934cea12756117441d2a33d0464ff3099be11ccb12946c62ece1126b9532eb8f6243a35d6fd171 + checksum: af17a6334e0b19323145482d829b664fcc3102cfbea9682753b9bc328840b9bc1968cd3cf64677cdc23c824194061f8fdf3a905d6f71431547a4ec413d557f92 languageName: node linkType: hard @@ -16337,14 +16337,14 @@ __metadata: languageName: node linkType: hard -"@smithy/url-parser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/url-parser@npm:2.1.1" +"@smithy/url-parser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/url-parser@npm:2.1.2" dependencies: - "@smithy/querystring-parser": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/querystring-parser": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 5c939f3ff9c53a0b7a0c5a1ac7641f229598d2bf9499e1abf4d33c1c1cd13bd5f7fcfffd00c366ca9f8092d28979a4a958b80f9bbc91e817e4d1940451e93489 + checksum: 83aca5a6474e85d835958caed5b486d5e6438682e6c17a5817ebda48ec9936c68b7b39c35a050f7eb4bd3902e83d8008b26d9fc5df7a168502d6ed93848005c6 languageName: node linkType: hard @@ -16395,42 +16395,42 @@ __metadata: languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-defaults-mode-browser@npm:2.1.1" +"@smithy/util-defaults-mode-browser@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-defaults-mode-browser@npm:2.1.2" dependencies: - "@smithy/property-provider": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/property-provider": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 bowser: ^2.11.0 tslib: ^2.5.0 - checksum: 5d3b11be1768410e24ad9829dc70bed9b50419f85a8ca934c6296e21e278d87f665cfdb603241ef749f80d154a2c4be26cd29338daecc625d31b30af8bd9c139 + checksum: bc0621f1d5ca46830a4b525def4b829e23336707af28e305758de7f97170024f387a9f3d73e2e29b033ea4963466373df644a304a87291c980159b31cf5ffbcf languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-defaults-mode-node@npm:2.2.0" +"@smithy/util-defaults-mode-node@npm:^2.2.1": + version: 2.2.1 + resolution: "@smithy/util-defaults-mode-node@npm:2.2.1" dependencies: - "@smithy/config-resolver": ^2.1.1 - "@smithy/credential-provider-imds": ^2.2.1 - "@smithy/node-config-provider": ^2.2.1 - "@smithy/property-provider": ^2.1.1 - "@smithy/smithy-client": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/config-resolver": ^2.1.2 + "@smithy/credential-provider-imds": ^2.2.2 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/property-provider": ^2.1.2 + "@smithy/smithy-client": ^2.4.0 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: c4a69b73bc46c3bb5ff4149b80bdfa79f4c25b82253d9c7168c9920066e12830e1bea324dce09414b09791fd0379bdc05c39117155d5b37a229d226962a95d5f + checksum: 672c13329e37d61170fb6c997b416fe9216efb6308b2e15560f5a96089bd839018d4705fb541aed0717dd1e1574e3dfc835f37a9608679713ee679d1d7c17642 languageName: node linkType: hard -"@smithy/util-endpoints@npm:^1.1.1": - version: 1.1.1 - resolution: "@smithy/util-endpoints@npm:1.1.1" +"@smithy/util-endpoints@npm:^1.1.2": + version: 1.1.2 + resolution: "@smithy/util-endpoints@npm:1.1.2" dependencies: - "@smithy/node-config-provider": ^2.2.1 - "@smithy/types": ^2.9.1 + "@smithy/node-config-provider": ^2.2.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 40619bf739c1fc959486946cb49319f34c9c4c5c19f46cdefc7ff8e7331b84f6ad7a4aeb8a0268f6d77d266ff5ec9df8d2244094dd79ae469983e9c07e43766a + checksum: 261f383e64116f767cc8e304a647c47261fee8425c43e517511e1bf8ec5e72652b8a12c65259ca4557f3fba477662606253d4cf89ccab5af184a05c59d2d735f languageName: node linkType: hard @@ -16443,40 +16443,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-middleware@npm:2.1.1" +"@smithy/util-middleware@npm:^2.1.1, @smithy/util-middleware@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-middleware@npm:2.1.2" dependencies: - "@smithy/types": ^2.9.1 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 404bb944202df70ba0ff8bb6ea105ead0a6b365d5ef7bfafbfc919df228823563818f0ee36f0f1e20462200da2fb8c8961e20b237e4e1bd9f77c38dd701f39ab + checksum: 8a05c05ba1358515aa6881189cd4a6a57701e8cd9e036c8d7219662fd12bce1695a4970c247314e0020f13bb506a558545ab5c519373647be89d79af08d5bcdf languageName: node linkType: hard -"@smithy/util-retry@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-retry@npm:2.1.1" +"@smithy/util-retry@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-retry@npm:2.1.2" dependencies: - "@smithy/service-error-classification": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/service-error-classification": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 1747c75f55a208f16104483cd76ec45200dedaa924868e84d4882b88f8b4a8d3a4422834359fd9bfba242e0e96a474349ac0a6f5d804fb15b15e8b639b6d2ad0 + checksum: 3be4b984b0f1daa54948fe158568a41003f725464ef32f0ccf32e02b566545364bd6f89a380a218397742edd1ad1d214906fab27debce531c137bfceca0c9c6d languageName: node linkType: hard -"@smithy/util-stream@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-stream@npm:2.1.1" +"@smithy/util-stream@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-stream@npm:2.1.2" dependencies: - "@smithy/fetch-http-handler": ^2.4.1 - "@smithy/node-http-handler": ^2.3.1 - "@smithy/types": ^2.9.1 + "@smithy/fetch-http-handler": ^2.4.2 + "@smithy/node-http-handler": ^2.4.0 + "@smithy/types": ^2.10.0 "@smithy/util-base64": ^2.1.1 "@smithy/util-buffer-from": ^2.1.1 "@smithy/util-hex-encoding": ^2.1.1 "@smithy/util-utf8": ^2.1.1 tslib: ^2.5.0 - checksum: 3a060226b8a506e722d0d8c1c4b7a2989241f7946c8acc892a8a70d92d9952cc8619b14bf686c9c822115d99159c6c16534bad2d72ecc2809a56f865224e82a6 + checksum: 8b95535323fcf3ce86cbb070791405afd4de1513a38fef209bfc4d5b2ed91ae16ae40393dd8a5f8b127194ec023cc264808f87beef0678219f56b2bcb580eb65 languageName: node linkType: hard @@ -16499,14 +16499,14 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^2.1.1": - version: 2.1.1 - resolution: "@smithy/util-waiter@npm:2.1.1" +"@smithy/util-waiter@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-waiter@npm:2.1.2" dependencies: - "@smithy/abort-controller": ^2.1.1 - "@smithy/types": ^2.9.1 + "@smithy/abort-controller": ^2.1.2 + "@smithy/types": ^2.10.0 tslib: ^2.5.0 - checksum: 52d9c82bb9684b6b11eeb2814fa1454514cb90aeeb87bfdf7c458613c13d18189712585486859c975824d08f2d1e3c817dd7e51c400531aaa479af8a06ea0bff + checksum: 089e777701ff2d6d8910f843c73de0d504221064401a02e56f104b5a50c66abfe8c8fa41c9e9ec62c8b6088f234cd995afccd9e2220fa7b7c8fcbf67fe343062 languageName: node linkType: hard From 37e734e865adb4bf64cb1ae683d58d6c914c8715 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 01:51:34 +0000 Subject: [PATCH 315/483] fix(deps): update dependency elastic-builder to v2.25.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 210e70b900..d57284da6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25915,8 +25915,8 @@ __metadata: linkType: hard "elastic-builder@npm:^2.16.0": - version: 2.24.0 - resolution: "elastic-builder@npm:2.24.0" + version: 2.25.0 + resolution: "elastic-builder@npm:2.25.0" dependencies: lodash.has: ^4.5.2 lodash.hasin: ^4.5.2 @@ -25926,7 +25926,7 @@ __metadata: lodash.isobject: ^3.0.2 lodash.isstring: ^4.0.1 lodash.omit: ^4.5.0 - checksum: 4bbfa66a179b78dbd90a0a3ee19cf7fc2105deca195c65d3047cf2ef2e6fa4925049038ed1f28d46a3ce96daa47f2cf8d924ea2e2fc0c585569a5a7acd09f2ed + checksum: 576b1060174cd5b62f5f802f9b947a3481ed0477c9c1ccbfe572f6ef4c3287c7ba0f5101cf6d32a69e1097e36fb95bded4066ef1ba195176f3b6b8c546d66e6d languageName: node linkType: hard From 086d7af53445157588a858796f231fa33f007e52 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 01:52:43 +0000 Subject: [PATCH 316/483] fix(deps): update dependency yaml to v2.4.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 210e70b900..c70476532e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46087,7 +46087,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.3.4, yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3": +"yaml@npm:2.3.4": version: 2.3.4 resolution: "yaml@npm:2.3.4" checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8 @@ -46101,6 +46101,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3": + version: 2.4.0 + resolution: "yaml@npm:2.4.0" + bin: + yaml: bin.mjs + checksum: 3c25ebae34ee702af772ebbd1855a980b1487cd21d6220d952592edb4f7d89322aafd14753d99924ba7076eb4c5b3d809c64bb532402b01af280f7af674277f1 + languageName: node + linkType: hard + "yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3": version: 18.1.3 resolution: "yargs-parser@npm:18.1.3" From 0fb419ba03ac366458bbd9bc40d87ec208832ec5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 02:29:25 +0000 Subject: [PATCH 317/483] fix(deps): update dependency uuid to v9 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-9850908.md | 38 ++++++++ packages/backend-common/package.json | 2 +- packages/backend-tasks/package.json | 2 +- packages/backend-test-utils/package.json | 2 +- plugins/auth-backend/package.json | 2 +- plugins/auth-node/package.json | 2 +- .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 4 +- .../package.json | 2 +- .../catalog-backend-module-ldap/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/catalog-backend/package.json | 4 +- plugins/code-coverage-backend/package.json | 2 +- .../example-todo-list-backend/package.json | 4 +- plugins/linguist-backend/package.json | 2 +- plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/package.json | 2 +- plugins/permission-common/package.json | 2 +- plugins/playlist-backend/package.json | 2 +- plugins/proxy-backend/package.json | 4 +- plugins/scaffolder-backend/package.json | 2 +- .../package.json | 2 +- plugins/search-backend-module-pg/package.json | 2 +- plugins/search-backend-node/package.json | 2 +- plugins/shortcuts/package.json | 2 +- plugins/signals-backend/package.json | 2 +- plugins/signals-node/package.json | 2 +- plugins/signals/package.json | 2 +- plugins/tech-insights-backend/package.json | 2 +- yarn.lock | 86 +++++++++---------- 36 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 .changeset/renovate-9850908.md diff --git a/.changeset/renovate-9850908.md b/.changeset/renovate-9850908.md new file mode 100644 index 0000000000..4911d08027 --- /dev/null +++ b/.changeset/renovate-9850908.md @@ -0,0 +1,38 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-signals-backend': patch +'@backstage/plugin-signals-node': patch +'@backstage/plugin-signals': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +Updated dependency `uuid` to `^9.0.0`. +Updated dependency `@types/uuid` to `^9.0.0`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 2fe5945750..a0753c6776 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -101,7 +101,7 @@ "pg": "^8.11.3", "raw-body": "^2.4.1", "tar": "^6.1.12", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "winston-transport": "^4.5.0", "yauzl": "^2.10.0", diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 6a8b9b4fc3..9f6743fe37 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -42,7 +42,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "zod": "^3.22.4" }, diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 4153fba211..6545ddbe20 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -58,7 +58,7 @@ "pg": "^8.11.3", "testcontainers": "^8.1.2", "textextensions": "^5.16.0", - "uuid": "^8.0.0" + "uuid": "^9.0.0" }, "peerDependencies": { "@types/jest": "*" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 967233e586..456cc18af2 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -87,7 +87,7 @@ "passport-microsoft": "^1.0.0", "passport-oauth2": "^1.6.1", "passport-onelogin-oauth": "^0.0.1", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index add7087ace..7830126365 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -57,6 +57,6 @@ "lodash": "^4.17.21", "msw": "^1.0.0", "supertest": "^6.1.3", - "uuid": "^8.0.0" + "uuid": "^9.0.0" } } diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 346daa2d09..19e4571d96 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -67,7 +67,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "p-limit": "^3.0.2", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 08b1dceeb5..2d92d7284f 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -57,7 +57,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index b5e75b517f..418e88a003 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -56,7 +56,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 46548b3df5..795652603d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "@types/node-fetch": "^2.5.12", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 3337176653..b0c0ff81ea 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "^11.2.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index b6a8f3acfe..6f3b933aa8 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -66,7 +66,7 @@ "lodash": "^4.17.21", "minimatch": "^9.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 661ed43f0e..76a20bd97f 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -54,14 +54,14 @@ "@backstage/plugin-catalog-node": "workspace:^", "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index e4d7b7efcf..02963f6bc1 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -65,7 +65,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "luxon": "^3.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9cae290571..7720e97aa6 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -42,7 +42,7 @@ "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 95070e6d78..b2c09bf201 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -63,7 +63,7 @@ "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "qs": "^6.9.4", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 4e127953bc..c971e58299 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -62,7 +62,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index fe72f0938c..7253f2db31 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -80,7 +80,7 @@ "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "prom-client": "^15.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "yn": "^4.0.0", @@ -95,7 +95,7 @@ "@types/glob": "^8.0.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "better-sqlite3": "^9.0.0", "luxon": "^3.0.0", "msw": "^1.0.0", diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index acdeab5a23..b44f608bbb 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -43,7 +43,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index d938d9816f..c6792746e9 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -40,14 +40,14 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "supertest": "^6.1.6" } } diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 0983b72f47..8b1d0104c1 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -52,7 +52,7 @@ "linguist-js": "^2.5.3", "luxon": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 809cab9973..8c11253a01 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -45,7 +45,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 9d2094beaf..9150ce1816 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -43,6 +43,6 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "knex": "^3.0.0", - "uuid": "^8.0.0" + "uuid": "^9.0.0" } } diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 7dd86b3b0d..204e146e56 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -45,7 +45,7 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index af26e90102..e7889830e1 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -47,7 +47,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.2.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0", "zod": "^3.22.4" diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 30ce530b9b..df9890b68c 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -58,7 +58,7 @@ "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", "morgan": "^1.10.0", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "yn": "^4.0.0", @@ -70,7 +70,7 @@ "@backstage/config-loader": "workspace:^", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", - "@types/uuid": "^8.0.0", + "@types/uuid": "^9.0.0", "@types/yup": "^0.29.13", "msw": "^1.0.0", "supertest": "^6.1.3" diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 4df6d6c89f..4fec5bdfaf 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -86,7 +86,7 @@ "p-limit": "^3.1.0", "p-queue": "^6.6.2", "prom-client": "^15.0.0", - "uuid": "^8.2.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 88b6d9542a..f177121a2e 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -57,7 +57,7 @@ "aws4": "^1.12.0", "elastic-builder": "^2.16.0", "lodash": "^4.17.21", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 2ba5331a4d..9344e1c5ba 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -54,7 +54,7 @@ "@backstage/plugin-search-common": "workspace:^", "knex": "^3.0.0", "lodash": "^4.17.21", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index b87bd15f7d..7ef7b7d2c5 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -56,7 +56,7 @@ "lodash": "^4.17.21", "lunr": "^2.3.9", "ndjson": "^2.0.0", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 5e3e6f9bb3..ef8bde1043 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -42,7 +42,7 @@ "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-hook-form": "^7.12.2", "react-use": "^17.2.4", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "zen-observable": "^0.10.0" }, "devDependencies": { diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index f8424ed0c1..4fcca7b0e6 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -39,7 +39,7 @@ "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", "node-fetch": "^2.6.7", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "winston": "^3.2.1", "ws": "^8.14.2", "yn": "^4.0.0" diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index c00696893d..03ef61739f 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -41,7 +41,7 @@ "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", "express": "^4.17.1", - "uuid": "^8.0.0", + "uuid": "^9.0.0", "ws": "^8.14.2" } } diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 22cc82ef54..5f2f4222c2 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -40,7 +40,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", "react-use": "^17.2.4", - "uuid": "^8.0.0" + "uuid": "^9.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index d39f8a3d6d..b1d84acae1 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -56,7 +56,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "semver": "^7.5.3", - "uuid": "^8.3.2", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/yarn.lock b/yarn.lock index df8ee83b70..b4606723c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3340,7 +3340,7 @@ __metadata: raw-body: ^2.4.1 supertest: ^6.1.3 tar: ^6.1.12 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 winston-transport: ^4.5.0 yauzl: ^2.10.0 @@ -3462,7 +3462,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 zod: ^3.22.4 @@ -3492,7 +3492,7 @@ __metadata: supertest: ^6.1.3 testcontainers: ^8.1.2 textextensions: ^5.16.0 - uuid: ^8.0.0 + uuid: ^9.0.0 peerDependencies: "@types/jest": "*" languageName: unknown @@ -4889,7 +4889,7 @@ __metadata: passport-oauth2: ^1.6.1 passport-onelogin-oauth: ^0.0.1 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -4919,7 +4919,7 @@ __metadata: node-fetch: ^2.6.7 passport: ^0.7.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 zod: ^3.22.4 zod-to-json-schema: ^3.21.4 @@ -5237,7 +5237,7 @@ __metadata: aws-sdk-client-mock-jest: ^3.0.0 luxon: ^3.0.0 p-limit: ^3.0.2 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 languageName: unknown @@ -5259,7 +5259,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5307,7 +5307,7 @@ __metadata: "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5330,7 +5330,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5371,7 +5371,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5418,7 +5418,7 @@ __metadata: minimatch: ^9.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5437,12 +5437,12 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@types/lodash": ^4.14.151 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5470,7 +5470,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 luxon: ^3.0.0 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5491,7 +5491,7 @@ __metadata: "@types/lodash": ^4.14.151 ldapjs: ^2.2.0 lodash: ^4.17.21 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5519,7 +5519,7 @@ __metadata: node-fetch: ^2.6.7 p-limit: ^3.0.2 qs: ^6.9.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5565,7 +5565,7 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 node-fetch: ^2.6.7 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -5628,7 +5628,7 @@ __metadata: "@types/glob": ^8.0.0 "@types/lodash": ^4.14.151 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 better-sqlite3: ^9.0.0 codeowners-utils: ^1.0.2 core-js: ^3.6.5 @@ -5646,7 +5646,7 @@ __metadata: p-limit: ^3.0.2 prom-client: ^15.0.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -6031,7 +6031,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 supertest: ^6.1.6 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 xml2js: ^0.6.0 yn: ^4.0.0 @@ -7450,7 +7450,7 @@ __metadata: luxon: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -7644,7 +7644,7 @@ __metadata: msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -7674,7 +7674,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 languageName: unknown linkType: soft @@ -7950,7 +7950,7 @@ __metadata: "@backstage/types": "workspace:^" cross-fetch: ^4.0.0 msw: ^1.0.0 - uuid: ^8.0.0 + uuid: ^9.0.0 zod: ^3.22.4 languageName: unknown linkType: soft @@ -8021,7 +8021,7 @@ __metadata: knex: ^3.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 - uuid: ^8.2.0 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 zod: ^3.22.4 @@ -8090,7 +8090,7 @@ __metadata: "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^0.19.3 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 "@types/yup": ^0.29.13 express: ^4.17.1 express-promise-router: ^4.1.0 @@ -8098,7 +8098,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 supertest: ^6.1.3 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 yn: ^4.0.0 @@ -8491,7 +8491,7 @@ __metadata: prom-client: ^15.0.0 strip-ansi: ^7.1.0 supertest: ^6.1.3 - uuid: ^8.2.0 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -8695,7 +8695,7 @@ __metadata: aws4: ^1.12.0 elastic-builder: ^2.16.0 lodash: ^4.17.21 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8732,7 +8732,7 @@ __metadata: "@backstage/plugin-search-common": "workspace:^" knex: ^3.0.0 lodash: ^4.17.21 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8799,7 +8799,7 @@ __metadata: lodash: ^4.17.21 lunr: ^2.3.9 ndjson: ^2.0.0 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 languageName: unknown linkType: soft @@ -8962,7 +8962,7 @@ __metadata: "@types/zen-observable": ^0.8.2 react-hook-form: ^7.12.2 react-use: ^17.2.4 - uuid: ^8.3.2 + uuid: ^9.0.0 zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -8991,7 +8991,7 @@ __metadata: msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 winston: ^3.2.1 ws: ^8.14.2 yn: ^4.0.0 @@ -9011,7 +9011,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/express": ^4.17.21 express: ^4.17.1 - uuid: ^8.0.0 + uuid: ^9.0.0 ws: ^8.14.2 languageName: unknown linkType: soft @@ -9054,7 +9054,7 @@ __metadata: jest-websocket-mock: ^2.5.0 msw: ^1.0.0 react-use: ^17.2.4 - uuid: ^8.0.0 + uuid: ^9.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 languageName: unknown @@ -9277,7 +9277,7 @@ __metadata: luxon: ^3.0.0 semver: ^7.5.3 supertest: ^6.1.3 - uuid: ^8.3.2 + uuid: ^9.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 yn: ^4.0.0 @@ -11925,11 +11925,11 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 - "@types/uuid": ^8.0.0 + "@types/uuid": ^9.0.0 express: ^4.17.1 express-promise-router: ^4.1.0 supertest: ^6.1.6 - uuid: ^8.3.2 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -19769,10 +19769,10 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^8.0.0": - version: 8.3.4 - resolution: "@types/uuid@npm:8.3.4" - checksum: 6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f +"@types/uuid@npm:^9.0.0": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 languageName: node linkType: hard @@ -44831,7 +44831,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.2.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": +"uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: From 568881fa78f16c8f82e34563a7bcb16c331f82f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:10:45 +0000 Subject: [PATCH 318/483] fix(deps): update dependency yauzl to v3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-ea48bac.md | 5 +++++ packages/backend-common/package.json | 2 +- yarn.lock | 21 ++++++--------------- 3 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 .changeset/renovate-ea48bac.md diff --git a/.changeset/renovate-ea48bac.md b/.changeset/renovate-ea48bac.md new file mode 100644 index 0000000000..ca56d7490a --- /dev/null +++ b/.changeset/renovate-ea48bac.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated dependency `yauzl` to `^3.0.0`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 2fe5945750..c1b7e181fd 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -104,7 +104,7 @@ "uuid": "^8.3.2", "winston": "^3.2.1", "winston-transport": "^4.5.0", - "yauzl": "^2.10.0", + "yauzl": "^3.0.0", "yn": "^4.0.0" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 3ce73c69e8..bdc37d7de8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3343,7 +3343,7 @@ __metadata: uuid: ^8.3.2 winston: ^3.2.1 winston-transport: ^4.5.0 - yauzl: ^2.10.0 + yauzl: ^3.0.0 yn: ^4.0.0 peerDependencies: pg-connection-string: ^2.3.0 @@ -28043,15 +28043,6 @@ __metadata: languageName: node linkType: hard -"fd-slicer@npm:~1.1.0": - version: 1.1.0 - resolution: "fd-slicer@npm:1.1.0" - dependencies: - pend: ~1.2.0 - checksum: c8585fd5713f4476eb8261150900d2cb7f6ff2d87f8feb306ccc8a1122efd152f1783bdb2b8dc891395744583436bfd8081d8e63ece0ec8687eeefea394d4ff2 - languageName: node - linkType: hard - "fecha@npm:^4.2.0": version: 4.2.0 resolution: "fecha@npm:4.2.0" @@ -46254,13 +46245,13 @@ __metadata: languageName: node linkType: hard -"yauzl@npm:^2.10.0": - version: 2.10.0 - resolution: "yauzl@npm:2.10.0" +"yauzl@npm:^3.0.0": + version: 3.1.0 + resolution: "yauzl@npm:3.1.0" dependencies: buffer-crc32: ~0.2.3 - fd-slicer: ~1.1.0 - checksum: 7f21fe0bbad6e2cb130044a5d1d0d5a0e5bf3d8d4f8c4e6ee12163ce798fee3de7388d22a7a0907f563ac5f9d40f8699a223d3d5c1718da90b0156da6904022b + pend: ~1.2.0 + checksum: 0464b49b0c10f0ab19136c917358b025c7f86bab0699a1f8afd681e09f3782688d3106b4e7eadfcc81e54779cca5c6de0843fabf5ceb07e1638ec2fd8371d0b7 languageName: node linkType: hard From 5097060df8204f42067d58ae47c7bcd6b2a2613b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Feb 2024 10:15:05 +0100 Subject: [PATCH 319/483] enter pre mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/pre.json | 280 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..8a921bf194 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,280 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.92", + "@backstage/app-defaults": "1.5.0", + "example-app-next": "0.0.6", + "app-next-example-plugin": "0.0.6", + "example-backend": "0.2.92", + "@backstage/backend-app-api": "0.5.11", + "@backstage/backend-common": "0.21.0", + "@backstage/backend-defaults": "0.2.10", + "@backstage/backend-dev-utils": "0.1.4", + "@backstage/backend-dynamic-feature-service": "0.2.0", + "example-backend-next": "0.0.20", + "@backstage/backend-openapi-utils": "0.1.3", + "@backstage/backend-plugin-api": "0.6.10", + "@backstage/backend-tasks": "0.5.15", + "@backstage/backend-test-utils": "0.3.0", + "@backstage/catalog-client": "1.6.0", + "@backstage/catalog-model": "1.4.4", + "@backstage/cli": "0.25.2", + "@backstage/cli-common": "0.1.13", + "@backstage/cli-node": "0.2.3", + "@backstage/codemods": "0.1.47", + "@backstage/config": "1.1.1", + "@backstage/config-loader": "1.6.2", + "@backstage/core-app-api": "1.12.0", + "@backstage/core-compat-api": "0.2.0", + "@backstage/core-components": "0.14.0", + "@backstage/core-plugin-api": "1.9.0", + "@backstage/create-app": "0.5.11", + "@backstage/dev-utils": "1.0.27", + "e2e-test": "0.2.12", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.3", + "@backstage/eslint-plugin": "0.1.5", + "@backstage/frontend-app-api": "0.6.0", + "@backstage/frontend-plugin-api": "0.6.0", + "@backstage/frontend-test-utils": "0.1.2", + "@backstage/integration": "1.9.0", + "@backstage/integration-aws-node": "0.1.9", + "@backstage/integration-react": "1.1.24", + "@backstage/release-manifests": "0.0.11", + "@backstage/repo-tools": "0.6.0", + "@techdocs/cli": "1.8.2", + "techdocs-cli-embedded-app": "0.2.91", + "@backstage/test-utils": "1.5.0", + "@backstage/theme": "0.5.1", + "@backstage/types": "1.1.1", + "@backstage/version-bridge": "1.0.7", + "@backstage/plugin-adr": "0.6.13", + "@backstage/plugin-adr-backend": "0.4.7", + "@backstage/plugin-adr-common": "0.2.20", + "@backstage/plugin-airbrake": "0.3.30", + "@backstage/plugin-airbrake-backend": "0.3.7", + "@backstage/plugin-allure": "0.1.46", + "@backstage/plugin-analytics-module-ga": "0.2.0", + "@backstage/plugin-analytics-module-ga4": "0.2.0", + "@backstage/plugin-analytics-module-newrelic-browser": "0.1.0", + "@backstage/plugin-apache-airflow": "0.2.20", + "@backstage/plugin-api-docs": "0.11.0", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.6", + "@backstage/plugin-apollo-explorer": "0.1.20", + "@backstage/plugin-app-backend": "0.3.58", + "@backstage/plugin-app-node": "0.1.10", + "@backstage/plugin-app-visualizer": "0.1.1", + "@backstage/plugin-auth-backend": "0.21.0", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.1.2", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.4", + "@backstage/plugin-auth-backend-module-github-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-google-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.1.7", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.2", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-okta-provider": "0.0.3", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.4", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.1.2", + "@backstage/plugin-auth-node": "0.4.4", + "@backstage/plugin-azure-devops": "0.3.12", + "@backstage/plugin-azure-devops-backend": "0.5.2", + "@backstage/plugin-azure-devops-common": "0.3.2", + "@backstage/plugin-azure-sites": "0.1.19", + "@backstage/plugin-azure-sites-backend": "0.2.0", + "@backstage/plugin-azure-sites-common": "0.1.2", + "@backstage/plugin-badges": "0.2.54", + "@backstage/plugin-badges-backend": "0.3.7", + "@backstage/plugin-bazaar": "0.2.22", + "@backstage/plugin-bazaar-backend": "0.3.8", + "@backstage/plugin-bitbucket-cloud-common": "0.2.16", + "@backstage/plugin-bitrise": "0.1.57", + "@backstage/plugin-catalog": "1.17.0", + "@backstage/plugin-catalog-backend": "1.17.0", + "@backstage/plugin-catalog-backend-module-aws": "0.3.4", + "@backstage/plugin-catalog-backend-module-azure": "0.1.29", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.1.3", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.25", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.23", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.10", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.26", + "@backstage/plugin-catalog-backend-module-github": "0.5.0", + "@backstage/plugin-catalog-backend-module-github-org": "0.1.4", + "@backstage/plugin-catalog-backend-module-gitlab": "0.3.7", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.14", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.25", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.17", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.27", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.15", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.7", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.3.7", + "@backstage/plugin-catalog-common": "1.0.21", + "@backstage/plugin-catalog-graph": "0.4.0", + "@backstage/plugin-catalog-import": "0.10.6", + "@backstage/plugin-catalog-node": "1.7.0", + "@backstage/plugin-catalog-react": "1.10.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.1.8", + "@backstage/plugin-cicd-statistics": "0.1.32", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.26", + "@backstage/plugin-circleci": "0.3.30", + "@backstage/plugin-cloudbuild": "0.4.0", + "@backstage/plugin-code-climate": "0.1.30", + "@backstage/plugin-code-coverage": "0.2.23", + "@backstage/plugin-code-coverage-backend": "0.2.24", + "@backstage/plugin-codescene": "0.1.22", + "@backstage/plugin-config-schema": "0.1.50", + "@backstage/plugin-cost-insights": "0.12.19", + "@backstage/plugin-cost-insights-common": "0.1.2", + "@backstage/plugin-devtools": "0.1.9", + "@backstage/plugin-devtools-backend": "0.2.7", + "@backstage/plugin-devtools-common": "0.1.8", + "@backstage/plugin-dynatrace": "9.0.0", + "@backstage/plugin-entity-feedback": "0.2.13", + "@backstage/plugin-entity-feedback-backend": "0.2.7", + "@backstage/plugin-entity-feedback-common": "0.1.3", + "@backstage/plugin-entity-validation": "0.1.15", + "@backstage/plugin-events-backend": "0.2.19", + "@backstage/plugin-events-backend-module-aws-sqs": "0.2.13", + "@backstage/plugin-events-backend-module-azure": "0.1.20", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.20", + "@backstage/plugin-events-backend-module-gerrit": "0.1.20", + "@backstage/plugin-events-backend-module-github": "0.1.20", + "@backstage/plugin-events-backend-module-gitlab": "0.1.20", + "@backstage/plugin-events-backend-test-utils": "0.1.20", + "@backstage/plugin-events-node": "0.2.19", + "@internal/plugin-todo-list": "1.0.22", + "@internal/plugin-todo-list-backend": "1.0.22", + "@internal/plugin-todo-list-common": "1.0.17", + "@backstage/plugin-explore": "0.4.16", + "@backstage/plugin-explore-backend": "0.0.20", + "@backstage/plugin-explore-common": "0.0.2", + "@backstage/plugin-explore-react": "0.0.36", + "@backstage/plugin-firehydrant": "0.2.14", + "@backstage/plugin-fossa": "0.2.62", + "@backstage/plugin-gcalendar": "0.3.23", + "@backstage/plugin-gcp-projects": "0.3.46", + "@backstage/plugin-git-release-manager": "0.3.42", + "@backstage/plugin-github-actions": "0.6.11", + "@backstage/plugin-github-deployments": "0.1.61", + "@backstage/plugin-github-issues": "0.2.19", + "@backstage/plugin-github-pull-requests-board": "0.1.24", + "@backstage/plugin-gitops-profiles": "0.3.45", + "@backstage/plugin-gocd": "0.1.36", + "@backstage/plugin-graphiql": "0.3.3", + "@backstage/plugin-graphql-voyager": "0.1.12", + "@backstage/plugin-home": "0.6.2", + "@backstage/plugin-home-react": "0.1.8", + "@backstage/plugin-ilert": "0.2.19", + "@backstage/plugin-jenkins": "0.9.5", + "@backstage/plugin-jenkins-backend": "0.3.4", + "@backstage/plugin-jenkins-common": "0.1.24", + "@backstage/plugin-kafka": "0.3.30", + "@backstage/plugin-kafka-backend": "0.3.8", + "@backstage/plugin-kubernetes": "0.11.5", + "@backstage/plugin-kubernetes-backend": "0.15.0", + "@backstage/plugin-kubernetes-cluster": "0.0.6", + "@backstage/plugin-kubernetes-common": "0.7.4", + "@backstage/plugin-kubernetes-node": "0.1.4", + "@backstage/plugin-kubernetes-react": "0.3.0", + "@backstage/plugin-lighthouse": "0.4.15", + "@backstage/plugin-lighthouse-backend": "0.4.2", + "@backstage/plugin-lighthouse-common": "0.1.4", + "@backstage/plugin-linguist": "0.1.15", + "@backstage/plugin-linguist-backend": "0.5.7", + "@backstage/plugin-linguist-common": "0.1.2", + "@backstage/plugin-microsoft-calendar": "0.1.12", + "@backstage/plugin-newrelic": "0.3.45", + "@backstage/plugin-newrelic-dashboard": "0.3.5", + "@backstage/plugin-nomad": "0.1.11", + "@backstage/plugin-nomad-backend": "0.1.12", + "@backstage/plugin-notifications": "0.0.1", + "@backstage/plugin-notifications-backend": "0.0.1", + "@backstage/plugin-notifications-common": "0.0.1", + "@backstage/plugin-notifications-node": "0.0.1", + "@backstage/plugin-octopus-deploy": "0.2.12", + "@backstage/plugin-opencost": "0.2.5", + "@backstage/plugin-org": "0.6.20", + "@backstage/plugin-org-react": "0.1.19", + "@backstage/plugin-pagerduty": "0.7.2", + "@backstage/plugin-periskop": "0.1.28", + "@backstage/plugin-periskop-backend": "0.2.8", + "@backstage/plugin-permission-backend": "0.5.33", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.7", + "@backstage/plugin-permission-common": "0.7.12", + "@backstage/plugin-permission-node": "0.7.21", + "@backstage/plugin-permission-react": "0.4.20", + "@backstage/plugin-playlist": "0.2.4", + "@backstage/plugin-playlist-backend": "0.3.14", + "@backstage/plugin-playlist-common": "0.1.14", + "@backstage/plugin-proxy-backend": "0.4.8", + "@backstage/plugin-puppetdb": "0.1.13", + "@backstage/plugin-rollbar": "0.4.30", + "@backstage/plugin-rollbar-backend": "0.1.55", + "@backstage/plugin-scaffolder": "1.18.0", + "@backstage/plugin-scaffolder-backend": "1.21.0", + "@backstage/plugin-scaffolder-backend-module-azure": "0.1.2", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.0", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.11", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.34", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.2", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-github": "0.2.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.27", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.18", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.31", + "@backstage/plugin-scaffolder-common": "1.5.0", + "@backstage/plugin-scaffolder-node": "0.3.0", + "@backstage/plugin-scaffolder-react": "1.8.0", + "@backstage/plugin-search": "1.4.6", + "@backstage/plugin-search-backend": "1.5.0", + "@backstage/plugin-search-backend-module-catalog": "0.1.14", + "@backstage/plugin-search-backend-module-elasticsearch": "1.3.13", + "@backstage/plugin-search-backend-module-explore": "0.1.14", + "@backstage/plugin-search-backend-module-pg": "0.5.19", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.3", + "@backstage/plugin-search-backend-module-techdocs": "0.1.14", + "@backstage/plugin-search-backend-node": "1.2.14", + "@backstage/plugin-search-common": "1.2.10", + "@backstage/plugin-search-react": "1.7.6", + "@backstage/plugin-sentry": "0.5.15", + "@backstage/plugin-shortcuts": "0.3.19", + "@backstage/plugin-signals": "0.0.1", + "@backstage/plugin-signals-backend": "0.0.1", + "@backstage/plugin-signals-node": "0.0.1", + "@backstage/plugin-signals-react": "0.0.1", + "@backstage/plugin-sonarqube": "0.7.12", + "@backstage/plugin-sonarqube-backend": "0.2.12", + "@backstage/plugin-sonarqube-react": "0.1.13", + "@backstage/plugin-splunk-on-call": "0.4.19", + "@backstage/plugin-stack-overflow": "0.1.25", + "@backstage/plugin-stack-overflow-backend": "0.2.14", + "@backstage/plugin-stackstorm": "0.1.11", + "@backstage/plugin-tech-insights": "0.3.22", + "@backstage/plugin-tech-insights-backend": "0.5.24", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.42", + "@backstage/plugin-tech-insights-common": "0.2.12", + "@backstage/plugin-tech-insights-node": "0.4.16", + "@backstage/plugin-tech-radar": "0.6.13", + "@backstage/plugin-techdocs": "1.10.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.27", + "@backstage/plugin-techdocs-backend": "1.9.3", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.5", + "@backstage/plugin-techdocs-node": "1.11.2", + "@backstage/plugin-techdocs-react": "1.1.16", + "@backstage/plugin-todo": "0.2.34", + "@backstage/plugin-todo-backend": "0.3.8", + "@backstage/plugin-user-settings": "0.8.1", + "@backstage/plugin-user-settings-backend": "0.2.9", + "@backstage/plugin-vault": "0.1.25", + "@backstage/plugin-vault-backend": "0.4.3", + "@backstage/plugin-vault-node": "0.1.3", + "@backstage/plugin-xcmetrics": "0.2.48" + }, + "changesets": [] +} From 05a4bbb5d0eaa7098b36b4f436bcf17c1ed21aa8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 10:17:50 +0100 Subject: [PATCH 320/483] beps/0003: add none credential and expiration times Signed-off-by: Patrik Oldsberg --- beps/0003-auth-architecture-evolution/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index fe3cc7bdb8..998eb08dbd 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -110,6 +110,8 @@ export type BackstageServicePrincipal = { export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; + principal: TPrincipal; }; @@ -132,6 +134,8 @@ export interface AuthService { type: TType, ): credentials is BackstageCredentials; + getNoneCredentials(): Promise>; + getOwnServiceCredentials(): Promise< BackstageCredentials >; @@ -228,9 +232,9 @@ export default createBackendPlugin({ // Endpoint that sets the cookie for the user router.get('/cookie', async (req, res) => { - await httpAuth.issueUserCookie(req); + const { expiresAt } = await httpAuth.issueUserCookie(req); - res.json({ ok: true }); + res.json({ expiresAt: expiresAt.toISOString() }); }); // Endpoint protected by cookie auth @@ -303,7 +307,7 @@ export interface HttpAuthService { // If credentials are not provided, they will be read from the request credentials?: BackstageCredentials; }, - ): Promise; + ): Promise<{ expiresAt: Date }>; } ``` From 1e416561fbe08afa07b9474a33cfe0287916168e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Feb 2024 10:37:53 +0100 Subject: [PATCH 321/483] Update packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Eric Peterson Signed-off-by: Fredrik Adelöw --- .../backend-common/src/auth/createLegacyAuthAdapters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts index db4461781b..43a503199b 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts @@ -56,7 +56,7 @@ describe('createLegacyAuthAdapters', () => { expect(ret.httpAuth).toBe(httpAuth); }); - it('should pass through userInfo if it provided', () => { + it('should pass through userInfo if it is provided', () => { const auth = {}; const userInfo = {}; const ret = createLegacyAuthAdapters({ From 744c0cbf9718b60a4ba300c25f59a412cb9ae9cc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 20 Feb 2024 14:14:30 +0100 Subject: [PATCH 322/483] refactor(search-backend): use credential for authorized search engines Signed-off-by: Camila Belo --- .changeset/dirty-apes-divide.md | 6 ++ .changeset/odd-toys-wonder.md | 5 ++ .changeset/six-grapes-sniff.md | 5 ++ .changeset/violet-rocks-rescue.md | 5 ++ packages/backend/package.json | 1 - packages/backend/src/plugins/search.ts | 3 +- .../backend/src/plugins/search.ts.hbs | 1 + .../api-report.md | 2 +- .../src/engines/ElasticSearchSearchEngine.ts | 2 +- .../search-backend-module-pg/api-report.md | 2 +- .../src/PgSearchEngine/PgSearchEngine.ts | 2 +- .../search-backend-node/api-report-alpha.md | 29 +++++++- plugins/search-backend-node/api-report.md | 25 ++++++- .../src/IndexBuilder.test.ts | 3 +- .../search-backend-node/src/IndexBuilder.ts | 2 +- plugins/search-backend-node/src/alpha.ts | 12 ++-- .../src/engines/LunrSearchEngine.test.ts | 6 +- .../src/engines/LunrSearchEngine.ts | 3 +- plugins/search-backend-node/src/index.ts | 3 + plugins/search-backend-node/src/types.ts | 57 +++++++++++++++- plugins/search-backend/api-report.md | 8 ++- plugins/search-backend/src/alpha.ts | 19 +++++- .../service/AuthorizedSearchEngine.test.ts | 2 +- .../src/service/AuthorizedSearchEngine.ts | 10 +-- .../search-backend/src/service/router.test.ts | 32 +++++++-- plugins/search-backend/src/service/router.ts | 32 ++++++--- .../src/service/standaloneServer.ts | 1 + plugins/search-common/api-report.md | 6 +- plugins/search-common/src/deprecated.ts | 68 +++++++++++++++++++ plugins/search-common/src/index.ts | 1 + plugins/search-common/src/types.ts | 49 +------------ yarn.lock | 1 - 32 files changed, 308 insertions(+), 95 deletions(-) create mode 100644 .changeset/dirty-apes-divide.md create mode 100644 .changeset/odd-toys-wonder.md create mode 100644 .changeset/six-grapes-sniff.md create mode 100644 .changeset/violet-rocks-rescue.md create mode 100644 plugins/search-common/src/deprecated.ts diff --git a/.changeset/dirty-apes-divide.md b/.changeset/dirty-apes-divide.md new file mode 100644 index 0000000000..e90bc7f5e1 --- /dev/null +++ b/.changeset/dirty-apes-divide.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +--- + +Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`. diff --git a/.changeset/odd-toys-wonder.md b/.changeset/odd-toys-wonder.md new file mode 100644 index 0000000000..aedd915c6e --- /dev/null +++ b/.changeset/odd-toys-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-common': patch +--- + +Deprecate `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` in favor of the types exported from `@backstage/plugin-search-backend-node`. diff --git a/.changeset/six-grapes-sniff.md b/.changeset/six-grapes-sniff.md new file mode 100644 index 0000000000..43e5ef04f6 --- /dev/null +++ b/.changeset/six-grapes-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +**BREAKING**: Update the router to use the new `auth` services. The router now requires a discovery service option to get credentials for the permission service. diff --git a/.changeset/violet-rocks-rescue.md b/.changeset/violet-rocks-rescue.md new file mode 100644 index 0000000000..91fbcdd871 --- /dev/null +++ b/.changeset/violet-rocks-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Exports `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` types. These new types were extracted from the `@backstage/plugin-search-common` package and the `token` property was deprecated in favor of the a new credentials one. diff --git a/packages/backend/package.json b/packages/backend/package.json index e6102b69cf..e2b8c5d50f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -72,7 +72,6 @@ "@backstage/plugin-search-backend-module-pg": "workspace:^", "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", - "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-signals-backend": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "@backstage/plugin-tech-insights-backend": "workspace:^", diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 7babfe7e1f..a5b976ec9d 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -23,9 +23,9 @@ import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-modu import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; import { IndexBuilder, + SearchEngine, LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; -import { SearchEngine } from '@backstage/plugin-search-common'; import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -117,6 +117,7 @@ export default async function createPlugin( return await createRouter({ engine: indexBuilder.getSearchEngine(), types: indexBuilder.getDocumentTypes(), + discovery: env.discovery, permissions: env.permissions, config: env.config, logger: env.logger, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs index 467ac60a5a..4149f67193 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs @@ -60,6 +60,7 @@ export default async function createPlugin( engine: indexBuilder.getSearchEngine(), types: indexBuilder.getDocumentTypes(), permissions: env.permissions, + discovery: env.discovery, config: env.config, logger: env.logger, }); diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 7de3a1857b..053c267f29 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -17,7 +17,7 @@ import { IndexableResultSet } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { SearchQuery } from '@backstage/plugin-search-common'; import { TransportRequestPromise } from '@opensearch-project/opensearch/lib/Transport'; import { TransportRequestPromise as TransportRequestPromise_2 } from '@elastic/elasticsearch/lib/Transport'; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 8ccbe9a4ec..f8c996df51 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -18,9 +18,9 @@ import { IndexableDocument, IndexableResult, IndexableResultSet, - SearchEngine, SearchQuery, } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { isEmpty, isNumber, isNaN as nan } from 'lodash'; import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws'; diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index 3c6096090e..21f2d7737d 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -10,7 +10,7 @@ import { IndexableResultSet } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { SearchQuery } from '@backstage/plugin-search-common'; // @public diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index 1a27b9f981..b6513a0a97 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -15,7 +15,7 @@ */ import { PluginDatabaseManager } from '@backstage/backend-common'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { SearchQuery, IndexableResultSet, diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index 74b7a4b4bc..a7ac52a218 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -3,12 +3,39 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { IndexableResultSet } from '@backstage/plugin-search-common'; import { RegisterCollatorParameters } from '@backstage/plugin-search-backend-node'; import { RegisterDecoratorParameters } from '@backstage/plugin-search-backend-node'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; import { ServiceRef } from '@backstage/backend-plugin-api'; +import { Writable } from 'stream'; + +// @public +export type QueryRequestOptions = + | { + token?: string; + } + | { + credentials: BackstageCredentials; + }; + +// @public +export type QueryTranslator = (query: SearchQuery) => unknown; + +// @public +export interface SearchEngine { + getIndexer(type: string): Promise; + query( + query: SearchQuery, + options?: QueryRequestOptions, + ): Promise; + setTranslator(translator: QueryTranslator): void; +} // @alpha export interface SearchEngineRegistryExtensionPoint { diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 734bf6e1c0..4109b12cf5 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -5,6 +5,7 @@ ```ts /// +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; @@ -14,9 +15,7 @@ import { IndexableResultSet } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { default as lunr_2 } from 'lunr'; import { Permission } from '@backstage/plugin-permission-common'; -import { QueryTranslator } from '@backstage/plugin-search-common'; import { Readable } from 'stream'; -import { SearchEngine } from '@backstage/plugin-search-common'; import { SearchQuery } from '@backstage/plugin-search-common'; import { TaskFunction } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; @@ -144,6 +143,18 @@ export type NewlineDelimitedJsonCollatorFactoryOptions = { visibilityPermission?: Permission; }; +// @public +export type QueryRequestOptions = + | { + token?: string; + } + | { + credentials: BackstageCredentials; + }; + +// @public +export type QueryTranslator = (query: SearchQuery) => unknown; + // @public export interface RegisterCollatorParameters { factory: DocumentCollatorFactory; @@ -170,6 +181,16 @@ export type ScheduleTaskParameters = { scheduledRunner: TaskRunner; }; +// @public +export interface SearchEngine { + getIndexer(type: string): Promise; + query( + query: SearchQuery, + options?: QueryRequestOptions, + ): Promise; + setTranslator(translator: QueryTranslator): void; +} + // @public export class TestPipeline { execute(): Promise; diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 4a2760dde1..70d00f6974 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -19,11 +19,10 @@ import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; import { DocumentCollatorFactory, DocumentDecoratorFactory, - SearchEngine, } from '@backstage/plugin-search-common'; import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; -import { LunrSearchEngine } from './index'; +import { LunrSearchEngine, SearchEngine } from './index'; class TestDocumentCollatorFactory implements DocumentCollatorFactory { readonly type: string = 'anything'; diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 907519f289..98c6853cb1 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -17,12 +17,12 @@ import { DocumentDecoratorFactory, DocumentTypeInfo, - SearchEngine, } from '@backstage/plugin-search-common'; import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './Scheduler'; import { + SearchEngine, IndexBuilderOptions, RegisterCollatorParameters, RegisterDecoratorParameters, diff --git a/plugins/search-backend-node/src/alpha.ts b/plugins/search-backend-node/src/alpha.ts index 943e11b580..a491e158b4 100644 --- a/plugins/search-backend-node/src/alpha.ts +++ b/plugins/search-backend-node/src/alpha.ts @@ -22,10 +22,7 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { - DocumentTypeInfo, - SearchEngine, -} from '@backstage/plugin-search-common'; +import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { @@ -33,8 +30,15 @@ import { RegisterDecoratorParameters, } from '@backstage/plugin-search-backend-node'; +import { SearchEngine } from './types'; import { IndexBuilder } from './IndexBuilder'; +export type { + SearchEngine, + QueryRequestOptions, + QueryTranslator, +} from './types'; + /** * @alpha * Options for build method on {@link SearchIndexService}. diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index 55c18cd6fc..e5347b3cb1 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -16,10 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; -import { - IndexableDocument, - SearchEngine, -} from '@backstage/plugin-search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { ConcreteLunrQuery, LunrSearchEngine, @@ -28,6 +25,7 @@ import { parseHighlightFields, } from './LunrSearchEngine'; import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +import { SearchEngine } from '../types'; import { TestPipeline } from '../test-utils'; /** diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 27cfae3fdf..a2db52f960 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -18,9 +18,8 @@ import { IndexableDocument, IndexableResultSet, SearchQuery, - QueryTranslator, - SearchEngine, } from '@backstage/plugin-search-common'; +import { SearchEngine, QueryTranslator } from '../types'; import { MissingIndexError } from '../errors'; import lunr from 'lunr'; import { v4 as uuid } from 'uuid'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index a188509c1c..78e4880305 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -33,6 +33,9 @@ export type { IndexBuilderOptions, RegisterCollatorParameters, RegisterDecoratorParameters, + SearchEngine, + QueryRequestOptions, + QueryTranslator, } from './types'; export * from './errors'; export * from './indexing'; diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index dfcf4d12ce..742d575ed3 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -14,12 +14,15 @@ * limitations under the License. */ +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { TaskRunner } from '@backstage/backend-tasks'; import { DocumentCollatorFactory, DocumentDecoratorFactory, - SearchEngine, + IndexableResultSet, + SearchQuery, } from '@backstage/plugin-search-common'; +import { Writable } from 'stream'; import { Logger } from 'winston'; /** @@ -57,3 +60,55 @@ export interface RegisterDecoratorParameters { */ factory: DocumentDecoratorFactory; } + +/** + * A type of function responsible for translating an abstract search query into + * a concrete query relevant to a particular search engine. + * @public + */ +export type QueryTranslator = (query: SearchQuery) => unknown; + +/** + * Options when querying a search engine. + * @public + */ +export type QueryRequestOptions = + | { + /** @deprecated use the `credentials` option instead. */ + token?: string; + } + | { + credentials: BackstageCredentials; + }; + +/** + * Interface that must be implemented by specific search engines, responsible + * for performing indexing and querying and translating abstract queries into + * concrete, search engine-specific queries. + * @public + */ +export interface SearchEngine { + /** + * Override the default translator provided by the SearchEngine. + */ + setTranslator(translator: QueryTranslator): void; + + /** + * Factory method for getting a search engine indexer for a given document + * type. + * + * @param type - The type or name of the document set for which an indexer + * should be retrieved. This corresponds to the `type` property on the + * document collator/decorator factories and will most often be used to + * identify an index or group to which documents should be written. + */ + getIndexer(type: string): Promise; + + /** + * Perform a search query against the SearchEngine. + */ + query( + query: SearchQuery, + options?: QueryRequestOptions, + ): Promise; +} diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index d6274d9a94..f868ed43a3 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -3,13 +3,16 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -18,8 +21,11 @@ export function createRouter(options: RouterOptions): Promise; export type RouterOptions = { engine: SearchEngine; types: Record; + discovery: DiscoveryService; permissions: PermissionEvaluator | PermissionAuthorizer; config: Config; logger: Logger; + auth?: AuthService; + httpAuth?: HttpAuthService; }; ``` diff --git a/plugins/search-backend/src/alpha.ts b/plugins/search-backend/src/alpha.ts index 8a9d4498bc..86bbcdeebe 100644 --- a/plugins/search-backend/src/alpha.ts +++ b/plugins/search-backend/src/alpha.ts @@ -22,6 +22,7 @@ import { loggerToWinstonLogger } from '@backstage/backend-common'; import { RegisterCollatorParameters, RegisterDecoratorParameters, + SearchEngine, LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; import { @@ -33,7 +34,6 @@ import { } from '@backstage/plugin-search-backend-node/alpha'; import { createRouter } from './service/router'; -import { SearchEngine } from '@backstage/plugin-search-common'; class SearchIndexRegistry implements SearchIndexRegistryExtensionPoint { private collators: RegisterCollatorParameters[] = []; @@ -94,11 +94,23 @@ export default createBackendPlugin({ deps: { logger: coreServices.logger, config: coreServices.rootConfig, + discovery: coreServices.discovery, permissions: coreServices.permissions, + auth: coreServices.auth, http: coreServices.httpRouter, + httpAuth: coreServices.httpAuth, searchIndexService: searchIndexServiceRef, }, - async init({ config, logger, permissions, http, searchIndexService }) { + async init({ + config, + logger, + discovery, + permissions, + auth, + http, + httpAuth, + searchIndexService, + }) { let searchEngine = searchEngineRegistry.getSearchEngine(); if (!searchEngine) { searchEngine = new LunrSearchEngine({ @@ -117,7 +129,10 @@ export default createBackendPlugin({ const router = await createRouter({ config, + discovery, permissions, + auth, + httpAuth, logger: loggerToWinstonLogger(logger), engine: searchEngine, types: searchIndexService.getDocumentTypes(), diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index b2355a7239..420af5744b 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -25,8 +25,8 @@ import { import { DocumentTypeInfo, IndexableDocument, - SearchEngine, } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { encodePageCursor, decodePageCursor, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index 8d285d053e..defb535f42 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -23,21 +23,23 @@ import { EvaluatePermissionRequest, EvaluatePermissionResponse, isResourcePermission, - PermissionEvaluator, QueryPermissionRequest, } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, IndexableResult, IndexableResultSet, + SearchQuery, +} from '@backstage/plugin-search-common'; +import { QueryRequestOptions, QueryTranslator, SearchEngine, - SearchQuery, -} from '@backstage/plugin-search-common'; +} from '@backstage/plugin-search-backend-node'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { Writable } from 'stream'; +import { PermissionsService } from '@backstage/backend-plugin-api'; export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { @@ -68,7 +70,7 @@ export class AuthorizedSearchEngine implements SearchEngine { constructor( private readonly searchEngine: SearchEngine, private readonly types: Record, - private readonly permissions: PermissionEvaluator, + private readonly permissions: PermissionsService, config: Config, ) { this.queryLatencyBudgetMs = diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 401488dcbf..7c89a8c750 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -14,16 +14,22 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + getVoidLogger, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { IndexBuilder } from '@backstage/plugin-search-backend-node'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { + IndexBuilder, + SearchEngine, +} from '@backstage/plugin-search-backend-node'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils'; import { Server } from 'http'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; const mockPermissionEvaluator: PermissionEvaluator = { authorize: () => { @@ -38,6 +44,16 @@ describe('createRouter', () => { let app: express.Express | Server; let mockSearchEngine: jest.Mocked; + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discovery: PluginEndpointDiscovery = { + async getBaseUrl() { + return mockBaseUrl; + }, + async getExternalBaseUrl() { + return mockBaseUrl; + }, + }; + beforeAll(async () => { const logger = getVoidLogger(); mockSearchEngine = { @@ -65,7 +81,10 @@ describe('createRouter', () => { search: { maxPageLimit: 200, maxTermLength: 20 }, }), permissions: mockPermissionEvaluator, + discovery, logger, + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = wrapInOpenApiTestServer(express().use(router)); }); @@ -227,7 +246,11 @@ describe('createRouter', () => { unknownKey2: 'unknownValue1', }; const secondArg = { - token: undefined, + credentials: mockCredentials.user(), + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'search', + }), }; expect(response.status).toEqual(200); expect(mockSearchEngine.query).toHaveBeenCalledWith(firstArg, secondArg); @@ -251,6 +274,7 @@ describe('createRouter', () => { types: indexBuilder.getDocumentTypes(), config: new ConfigReader({ permissions: { enabled: false } }), permissions: mockPermissionEvaluator, + discovery, logger, }); app = express().use(router); diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 876cb5d103..9f811d5d06 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -17,11 +17,13 @@ import express from 'express'; import { Logger } from 'winston'; import { z } from 'zod'; -import { errorHandler } from '@backstage/backend-common'; +import { + createLegacyAuthAdapters, + errorHandler, +} from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { PermissionAuthorizer, PermissionEvaluator, @@ -32,9 +34,14 @@ import { IndexableResultSet, SearchResultSet, } from '@backstage/plugin-search-common'; -import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; import { createOpenApiRouter } from '../schema/openapi.generated'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; const jsonObjectSchema: z.ZodSchema = z.lazy(() => { const jsonValueSchema: z.ZodSchema = z.lazy(() => @@ -57,9 +64,12 @@ const jsonObjectSchema: z.ZodSchema = z.lazy(() => { export type RouterOptions = { engine: SearchEngine; types: Record; + discovery: DiscoveryService; permissions: PermissionEvaluator | PermissionAuthorizer; config: Config; logger: Logger; + auth?: AuthService; + httpAuth?: HttpAuthService; }; const defaultMaxPageLimit = 100; @@ -75,6 +85,8 @@ export async function createRouter( const router = await createOpenApiRouter(); const { engine: inputEngine, types, permissions, config, logger } = options; + const { auth, httpAuth } = createLegacyAuthAdapters(options); + const maxPageLimit = config.getOptionalNumber('search.maxPageLimit') ?? defaultMaxPageLimit; @@ -169,12 +181,16 @@ export async function createRouter( }`, ); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - try { - const resultSet = await engine?.query(query, { token }); + const credentials = await httpAuth.credentials(req); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'search', + }); + const resultSet = await engine?.query(query, { + token, + credentials, + }); res.json(filterResultSet(toSearchResults(resultSet))); } catch (error) { diff --git a/plugins/search-backend/src/service/standaloneServer.ts b/plugins/search-backend/src/service/standaloneServer.ts index 9e4294d39f..09fa4f87ee 100644 --- a/plugins/search-backend/src/service/standaloneServer.ts +++ b/plugins/search-backend/src/service/standaloneServer.ts @@ -57,6 +57,7 @@ export async function startStandaloneServer( const router = await createRouter({ engine: indexBuilder.getSearchEngine(), types: indexBuilder.getDocumentTypes(), + discovery, permissions, config, logger, diff --git a/plugins/search-common/api-report.md b/plugins/search-common/api-report.md index 2006689ab9..497defc46d 100644 --- a/plugins/search-common/api-report.md +++ b/plugins/search-common/api-report.md @@ -42,12 +42,12 @@ export type IndexableResult = Result; // @public (undocumented) export type IndexableResultSet = ResultSet; -// @public +// @public @deprecated export type QueryRequestOptions = { token?: string; }; -// @public +// @public @deprecated export type QueryTranslator = (query: SearchQuery) => unknown; // @public (undocumented) @@ -87,7 +87,7 @@ export interface SearchDocument { title: string; } -// @public +// @public @deprecated export interface SearchEngine { getIndexer(type: string): Promise; query( diff --git a/plugins/search-common/src/deprecated.ts b/plugins/search-common/src/deprecated.ts new file mode 100644 index 0000000000..8d76d3a22d --- /dev/null +++ b/plugins/search-common/src/deprecated.ts @@ -0,0 +1,68 @@ +/* + * 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 { Writable } from 'stream'; +import { SearchQuery, IndexableResultSet } from './types'; + +/** + * A type of function responsible for translating an abstract search query into + * a concrete query relevant to a particular search engine. + * @public + * @deprecated Import from `@backstage/plugin-search-backend-node` instead + */ +export type QueryTranslator = (query: SearchQuery) => unknown; + +/** + * Options when querying a search engine. + * @public + * @deprecated Import from `@backstage/plugin-search-backend-node` instead + */ +export type QueryRequestOptions = { + token?: string; +}; + +/** + * Interface that must be implemented by specific search engines, responsible + * for performing indexing and querying and translating abstract queries into + * concrete, search engine-specific queries. + * @public + * @deprecated Import from `@backstage/plugin-search-backend-node` instead + */ +export interface SearchEngine { + /** + * Override the default translator provided by the SearchEngine. + */ + setTranslator(translator: QueryTranslator): void; + + /** + * Factory method for getting a search engine indexer for a given document + * type. + * + * @param type - The type or name of the document set for which an indexer + * should be retrieved. This corresponds to the `type` property on the + * document collator/decorator factories and will most often be used to + * identify an index or group to which documents should be written. + */ + getIndexer(type: string): Promise; + + /** + * Perform a search query against the SearchEngine. + */ + query( + query: SearchQuery, + options?: QueryRequestOptions, + ): Promise; +} diff --git a/plugins/search-common/src/index.ts b/plugins/search-common/src/index.ts index e71f8cd660..5d8ae2d42f 100644 --- a/plugins/search-common/src/index.ts +++ b/plugins/search-common/src/index.ts @@ -21,3 +21,4 @@ */ export * from './types'; +export * from './deprecated'; diff --git a/plugins/search-common/src/types.ts b/plugins/search-common/src/types.ts index 2ea4a490ca..9d1f9aa887 100644 --- a/plugins/search-common/src/types.ts +++ b/plugins/search-common/src/types.ts @@ -16,7 +16,7 @@ import { Permission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; -import { Readable, Transform, Writable } from 'stream'; +import { Readable, Transform } from 'stream'; /** * @public @@ -206,50 +206,3 @@ export interface DocumentDecoratorFactory { */ getDecorator(): Promise; } - -/** - * A type of function responsible for translating an abstract search query into - * a concrete query relevant to a particular search engine. - * @public - */ -export type QueryTranslator = (query: SearchQuery) => unknown; - -/** - * Options when querying a search engine. - * @public - */ -export type QueryRequestOptions = { - token?: string; -}; - -/** - * Interface that must be implemented by specific search engines, responsible - * for performing indexing and querying and translating abstract queries into - * concrete, search engine-specific queries. - * @public - */ -export interface SearchEngine { - /** - * Override the default translator provided by the SearchEngine. - */ - setTranslator(translator: QueryTranslator): void; - - /** - * Factory method for getting a search engine indexer for a given document - * type. - * - * @param type - The type or name of the document set for which an indexer - * should be retrieved. This corresponds to the `type` property on the - * document collator/decorator factories and will most often be used to - * identify an index or group to which documents should be written. - */ - getIndexer(type: string): Promise; - - /** - * Perform a search query against the SearchEngine. - */ - query( - query: SearchQuery, - options?: QueryRequestOptions, - ): Promise; -} diff --git a/yarn.lock b/yarn.lock index 5aa82df2e2..6f5818920a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27469,7 +27469,6 @@ __metadata: "@backstage/plugin-search-backend-module-pg": "workspace:^" "@backstage/plugin-search-backend-module-techdocs": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" - "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-signals-backend": "workspace:^" "@backstage/plugin-signals-node": "workspace:^" "@backstage/plugin-tech-insights-backend": "workspace:^" From bd37c85bfae074ed059218e84ebc73154f0d99e3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 10:53:45 +0100 Subject: [PATCH 323/483] scaffolder-backend-module-gitlab: clear mocks in test Signed-off-by: Patrik Oldsberg --- .../src/actions/gitlabMergeRequest.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index 71a2273aed..0883a5e9f0 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -78,6 +78,8 @@ describe('createGitLabMergeRequest', () => { const workspacePath = mockDir.resolve('workspace'); beforeEach(() => { + jest.clearAllMocks(); + mockDir.clear(); const config = new ConfigReader({ From b49374c65e7ed5cb9d9f9f633f52f2680819aecb Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 26 Feb 2024 10:57:57 +0100 Subject: [PATCH 324/483] feat(create-app): update search template Signed-off-by: Camila Belo --- .changeset/polite-parrots-clap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-parrots-clap.md diff --git a/.changeset/polite-parrots-clap.md b/.changeset/polite-parrots-clap.md new file mode 100644 index 0000000000..de05fa949b --- /dev/null +++ b/.changeset/polite-parrots-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Update the search backend template to forward env discovery to the router. From 19e3a21e5f75c156b0a6b7392ce11422bfc30aab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 11:00:46 +0100 Subject: [PATCH 325/483] Update plugins/playlist-backend/src/service/router.ts Co-authored-by: Phil Kuang Signed-off-by: Patrik Oldsberg --- plugins/playlist-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/playlist-backend/src/service/router.ts b/plugins/playlist-backend/src/service/router.ts index 8fa9983d0b..b73c7279f7 100644 --- a/plugins/playlist-backend/src/service/router.ts +++ b/plugins/playlist-backend/src/service/router.ts @@ -232,7 +232,7 @@ export async function createRouter( targetPluginId: 'catalog', }); - // TODO(kuanpg): entities in this playlist that no longer exist in the catalog will be + // TODO(kuangp): entities in this playlist that no longer exist in the catalog will be // excluded from this response, we need a way to clean up these orphaned refs potentially // via catalog events (https://github.com/backstage/backstage/issues/8219) // From 1b4fd09aeaa0ea04cb4144ced917175d28653d38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:05:43 +0000 Subject: [PATCH 326/483] fix(deps): update dependency yup to v1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .changeset/renovate-7aa519f.md | 6 ++++ plugins/cost-insights/package.json | 2 +- plugins/proxy-backend/package.json | 2 +- yarn.lock | 47 ++++++++++++++---------------- 4 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 .changeset/renovate-7aa519f.md diff --git a/.changeset/renovate-7aa519f.md b/.changeset/renovate-7aa519f.md new file mode 100644 index 0000000000..763d94217b --- /dev/null +++ b/.changeset/renovate-7aa519f.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-proxy-backend': patch +--- + +Updated dependency `yup` to `^1.0.0`. diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 4bc0454ab0..50c6119655 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -58,7 +58,7 @@ "react-use": "^17.2.4", "recharts": "^2.5.0", "regression": "^2.0.1", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index df9890b68c..5e4ed1e728 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -62,7 +62,7 @@ "winston": "^3.2.1", "yaml": "^2.0.0", "yn": "^4.0.0", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/yarn.lock b/yarn.lock index f71d95fbcd..86dd8563f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3144,7 +3144,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.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.8, @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.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.8, @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.23.8 resolution: "@babel/runtime@npm:7.23.8" dependencies: @@ -6170,7 +6170,7 @@ __metadata: react-use: ^17.2.4 recharts: ^2.5.0 regression: ^2.0.1 - yup: ^0.32.9 + yup: ^1.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -8102,7 +8102,7 @@ __metadata: winston: ^3.2.1 yaml: ^2.0.0 yn: ^4.0.0 - yup: ^0.32.9 + yup: ^1.0.0 languageName: unknown linkType: soft @@ -18857,7 +18857,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.14.151, @types/lodash@npm:^4.14.173, @types/lodash@npm:^4.14.175": +"@types/lodash@npm:^4.14.151, @types/lodash@npm:^4.14.173": version: 4.14.202 resolution: "@types/lodash@npm:4.14.202" checksum: a91acf3564a568c6f199912f3eb2c76c99c5a0d7e219394294213b3f2d54f672619f0fde4da22b29dc5d4c31457cd799acc2e5cb6bd90f9af04a1578483b6ff7 @@ -35657,13 +35657,6 @@ __metadata: languageName: node linkType: hard -"nanoclone@npm:^0.2.1": - version: 0.2.1 - resolution: "nanoclone@npm:0.2.1" - checksum: 96b2954e22f70561f41e20d69856266c65583c2a441dae108f1dc71b716785d2c8038dac5f1d5e92b117aed3825f526b53139e2e5d6e6db8a77cfa35b3b8bf40 - languageName: node - linkType: hard - "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -38792,10 +38785,10 @@ __metadata: languageName: node linkType: hard -"property-expr@npm:^2.0.4": - version: 2.0.4 - resolution: "property-expr@npm:2.0.4" - checksum: 7ac142e189f0feef685f327f582efe13bfbc24a0b6e2328afdb38520bc140caa5f91dfa9529f2539b4468d85dc83a593e1ef0e0f7401b525368bb634b323bf54 +"property-expr@npm:^2.0.5": + version: 2.0.6 + resolution: "property-expr@npm:2.0.6" + checksum: 89977f4bb230736c1876f460dd7ca9328034502fd92e738deb40516d16564b850c0bbc4e052c3df88b5b8cd58e51c93b46a94bea049a3f23f4a022c038864cab languageName: node linkType: hard @@ -43506,6 +43499,13 @@ __metadata: languageName: node linkType: hard +"tiny-case@npm:^1.0.3": + version: 1.0.3 + resolution: "tiny-case@npm:1.0.3" + checksum: 3f7a30c39d5b0e1bc097b0b271bec14eb5b836093db034f35a0de26c14422380b50dc12bfd37498cf35b192f5df06f28a710712c87ead68872a9e37ad6f6049d + languageName: node + linkType: hard + "tiny-emitter@npm:^2.0.0": version: 2.1.0 resolution: "tiny-emitter@npm:2.1.0" @@ -46349,18 +46349,15 @@ __metadata: languageName: node linkType: hard -"yup@npm:^0.32.9": - version: 0.32.11 - resolution: "yup@npm:0.32.11" +"yup@npm:^1.0.0": + version: 1.3.3 + resolution: "yup@npm:1.3.3" dependencies: - "@babel/runtime": ^7.15.4 - "@types/lodash": ^4.14.175 - lodash: ^4.17.21 - lodash-es: ^4.17.21 - nanoclone: ^0.2.1 - property-expr: ^2.0.4 + property-expr: ^2.0.5 + tiny-case: ^1.0.3 toposort: ^2.0.2 - checksum: 43a16786b47cc910fed4891cebdd89df6d6e31702e9462e8f969c73eac88551ce750732608012201ea6b93802c8847cb0aa27b5d57370640f4ecf30f9f97d4b0 + type-fest: ^2.19.0 + checksum: 7b9e19fedc85deb8cffd6b24617f79542c904f11a9c4ca8bb72fb003e61bd6c5278cc799fa9421c6a1a96ff04e7117bf0c000c9f10b543d6cd32a1e35e8f5f65 languageName: node linkType: hard From 15ba00ff7dc25828d797f2615b09e49c522d3090 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Feb 2024 13:24:42 +0100 Subject: [PATCH 327/483] catalog-backend: migrate to support new auth services Signed-off-by: Patrik Oldsberg --- .changeset/purple-kiwis-complain.md | 5 + plugins/catalog-backend/api-report.md | 10 +- plugins/catalog-backend/src/catalog/types.ts | 19 +-- .../service/AuthorizedEntitiesCatalog.test.ts | 79 ++++++----- .../src/service/AuthorizedEntitiesCatalog.ts | 41 +++--- .../service/AuthorizedLocationService.test.ts | 63 ++++----- .../src/service/AuthorizedLocationService.ts | 45 ++++--- .../service/AuthorizedRefreshService.test.ts | 5 +- .../src/service/AuthorizedRefreshService.ts | 10 +- .../src/service/CatalogBuilder.ts | 49 +++++-- .../src/service/CatalogPlugin.ts | 9 ++ .../service/DefaultEntitiesCatalog.test.ts | 93 ++++++++++--- .../src/service/DefaultRefreshService.test.ts | 10 +- .../src/service/createRouter.test.ts | 127 +++++++++--------- .../src/service/createRouter.ts | 77 ++++------- .../request/parseQueryEntitiesParams.ts | 6 +- plugins/catalog-backend/src/service/types.ts | 17 ++- 17 files changed, 396 insertions(+), 269 deletions(-) create mode 100644 .changeset/purple-kiwis-complain.md diff --git a/.changeset/purple-kiwis-complain.md b/.changeset/purple-kiwis-complain.md new file mode 100644 index 0000000000..a5bcb3ed67 --- /dev/null +++ b/.changeset/purple-kiwis-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 92a2fbb280..ef2379766d 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -11,6 +11,7 @@ import { AnalyzeLocationGenerateEntity as AnalyzeLocationGenerateEntity_2 } from import { AnalyzeLocationRequest as AnalyzeLocationRequest_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeLocationResponse as AnalyzeLocationResponse_2 } from '@backstage/plugin-catalog-common'; import { AnalyzeOptions as AnalyzeOptions_2 } from '@backstage/plugin-catalog-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogCollatorEntityTransformer as CatalogCollatorEntityTransformer_2 } from '@backstage/plugin-search-backend-module-catalog'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; @@ -28,6 +29,7 @@ import { Config } from '@backstage/config'; import { DefaultCatalogCollatorFactory as DefaultCatalogCollatorFactory_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DefaultCatalogCollatorFactoryOptions as DefaultCatalogCollatorFactoryOptions_2 } from '@backstage/plugin-search-backend-module-catalog'; import { DeferredEntity as DeferredEntity_2 } from '@backstage/plugin-catalog-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EntitiesSearchFilter as EntitiesSearchFilter_2 } from '@backstage/plugin-catalog-node'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter as EntityFilter_2 } from '@backstage/plugin-catalog-node'; @@ -38,15 +40,16 @@ import { EntityProviderMutation as EntityProviderMutation_2 } from '@backstage/p import { EntityRelationSpec as EntityRelationSpec_2 } from '@backstage/plugin-catalog-node'; import { EventBroker } from '@backstage/plugin-events-node'; import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; import { locationSpecToLocationEntity as locationSpecToLocationEntity_2 } from '@backstage/plugin-catalog-node'; import { locationSpecToMetadataName as locationSpecToMetadataName_2 } from '@backstage/plugin-catalog-node'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverParams as PlaceholderResolverParams_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverRead as PlaceholderResolverRead_2 } from '@backstage/plugin-catalog-node'; @@ -189,8 +192,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; // @public diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index e9f5f154c2..9cc83098b3 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; @@ -48,7 +49,7 @@ export type EntitiesRequest = { fields?: (entity: Entity) => Entity; order?: EntityOrder[]; pagination?: EntityPagination; - authorizationToken?: string; + credentials: BackstageCredentials; }; export type EntitiesResponse = { @@ -75,9 +76,9 @@ export interface EntitiesBatchRequest { */ fields?: (entity: Entity) => Entity; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } export interface EntitiesBatchResponse { @@ -115,9 +116,9 @@ export interface EntityFacetsRequest { */ facets: string[]; /** - * The optional token that authorizes the action. + * The credentials that authorizes the action. */ - authorizationToken?: string; + credentials: BackstageCredentials; } /** @@ -157,7 +158,7 @@ export interface EntitiesCatalog { */ removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -167,7 +168,7 @@ export interface EntitiesCatalog { */ entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; /** @@ -192,7 +193,7 @@ export type QueryEntitiesRequest = * for the current and the next pagination requests. */ export interface QueryEntitiesInitialRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; filter?: EntityFilter; @@ -208,7 +209,7 @@ export interface QueryEntitiesInitialRequest { * move forward or backward on the data. */ export interface QueryEntitiesCursorRequest { - authorizationToken?: string; + credentials: BackstageCredentials; fields?: (entity: Entity) => Entity; limit?: number; cursor: Cursor; diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 4fe0311fff..3e2dee7ae0 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -23,6 +23,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; import { Cursor, QueryEntitiesResponse } from '../catalog/types'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedEntitiesCatalog', () => { const fakeCatalog = { @@ -60,7 +61,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.entities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ entities: [], @@ -77,10 +78,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -91,10 +92,10 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.entities({ authorizationToken: 'abcd' }); + await catalog.entities({ credentials: mockCredentials.none() }); expect(fakeCatalog.entities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -109,7 +110,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).resolves.toEqual({ items: [null], @@ -132,12 +133,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -150,12 +151,12 @@ describe('AuthorizedEntitiesCatalog', () => { await catalog.entitiesBatch({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: ['component:default/component-a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); @@ -169,7 +170,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect( catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }), ).resolves.toEqual({ @@ -188,12 +189,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(); await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -243,12 +244,12 @@ describe('AuthorizedEntitiesCatalog', () => { const catalog = createCatalog(isEntityKind); let response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'name', values: ['name'] }, }); expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, }); @@ -276,12 +277,12 @@ describe('AuthorizedEntitiesCatalog', () => { orderFieldValues: ['a', null], }; response = await catalog.queryEntities({ - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor, }); expect(fakeCatalog.queryEntities).toHaveBeenNthCalledWith(2, { - authorizationToken: 'abcd', + credentials: mockCredentials.none(), cursor: { ...cursor, filter: { allOf: [{ key: 'kind', values: ['b'] }, requestFilter] }, @@ -324,7 +325,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -343,7 +346,9 @@ describe('AuthorizedEntitiesCatalog', () => { ); await expect(() => - catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }), + catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }), ).rejects.toThrow(NotAllowedError); }); @@ -363,9 +368,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([isEntityKind]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); it('calls underlying catalog method on ALLOW', async () => { @@ -383,9 +392,13 @@ describe('AuthorizedEntitiesCatalog', () => { createConditionTransformer([]), ); - await catalog.removeEntityByUid('uid', { authorizationToken: 'abcd' }); + await catalog.removeEntityByUid('uid', { + credentials: mockCredentials.none(), + }); - expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid'); + expect(fakeCatalog.removeEntityByUid).toHaveBeenCalledWith('uid', { + credentials: mockCredentials.none(), + }); }); }); @@ -398,7 +411,7 @@ describe('AuthorizedEntitiesCatalog', () => { await expect(() => catalog.entityAncestry('backstage:default/component', { - authorizationToken: 'Bearer abcd', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -443,7 +456,7 @@ describe('AuthorizedEntitiesCatalog', () => { const ancestryResult = await catalog.entityAncestry( 'backstage:default/a', - { authorizationToken: 'Bearer abcd' }, + { credentials: mockCredentials.none() }, ); expect(ancestryResult).toEqual({ @@ -476,7 +489,7 @@ describe('AuthorizedEntitiesCatalog', () => { expect( await catalog.facets({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }), ).toEqual({ facets: { a: [] }, @@ -492,11 +505,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(isEntityKind); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), filter: { key: 'kind', values: ['b'] }, }); }); @@ -507,11 +523,14 @@ describe('AuthorizedEntitiesCatalog', () => { ]); const catalog = createCatalog(); - await catalog.facets({ facets: ['a'], authorizationToken: 'abcd' }); + await catalog.facets({ + facets: ['a'], + credentials: mockCredentials.none(), + }); expect(fakeCatalog.facets).toHaveBeenCalledWith({ facets: ['a'], - authorizationToken: 'abcd', + credentials: mockCredentials.none(), }); }); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index f302a2b75a..5358e90825 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -20,10 +20,7 @@ import { catalogEntityReadPermission, } from '@backstage/plugin-catalog-common/alpha'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ConditionTransformer } from '@backstage/plugin-permission-node'; import { Cursor, @@ -41,19 +38,23 @@ import { import { basicEntityFilter } from './request'; import { isQueryEntitiesCursorRequest } from './util'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedEntitiesCatalog implements EntitiesCatalog { constructor( private readonly entitiesCatalog: EntitiesCatalog, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, private readonly transformConditions: ConditionTransformer, ) {} - async entities(request?: EntitiesRequest): Promise { + async entities(request: EntitiesRequest): Promise { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -85,7 +86,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -116,7 +117,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request.authorizationToken }, + { credentials: request.credentials }, ) )[0]; @@ -186,12 +187,12 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { async removeEntityByUid( uid: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizeResponse = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeResponse.result === AuthorizeResult.DENY) { @@ -202,6 +203,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { authorizeResponse.conditions, ); const { entities } = await this.entitiesCatalog.entities({ + credentials: options.credentials, filter: { allOf: [permissionFilter, basicEntityFilter({ 'metadata.uid': uid })], }, @@ -210,30 +212,35 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { throw new NotAllowedError(); } } - return this.entitiesCatalog.removeEntityByUid(uid); + return this.entitiesCatalog.removeEntityByUid(uid, { + credentials: options.credentials, + }); } async entityAncestry( entityRef: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const rootEntityAuthorizeResponse = ( await this.permissionApi.authorize( [{ permission: catalogEntityReadPermission, resourceRef: entityRef }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (rootEntityAuthorizeResponse.result === AuthorizeResult.DENY) { throw new NotAllowedError(); } - const ancestryResult = await this.entitiesCatalog.entityAncestry(entityRef); + const ancestryResult = await this.entitiesCatalog.entityAncestry( + entityRef, + { credentials: options.credentials }, + ); const authorizeResponse = await this.permissionApi.authorize( ancestryResult.items.map(item => ({ permission: catalogEntityReadPermission, resourceRef: stringifyEntityRef(item.entity), })), - { token: options?.authorizationToken }, + { credentials: options.credentials }, ); const unauthorizedAncestryItems = ancestryResult.items.filter( (_, index) => authorizeResponse[index].result === AuthorizeResult.DENY, @@ -268,7 +275,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], - { token: request?.authorizationToken }, + { credentials: request.credentials }, ) )[0]; diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts index c2ffee8003..2eae1c7553 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.test.ts @@ -17,6 +17,7 @@ import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { AuthorizedLocationService } from './AuthorizedLocationService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedLocationService', () => { const fakeLocationService = { @@ -45,6 +46,10 @@ describe('AuthorizedLocationService', () => { const createService = () => new AuthorizedLocationService(fakeLocationService, fakePermissionApi); + const mockOptions = { + credentials: mockCredentials.none(), + }; + afterEach(() => { jest.resetAllMocks(); }); @@ -55,13 +60,12 @@ describe('AuthorizedLocationService', () => { const service = createService(); const spec = { type: 'type', target: 'target' }; - await service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }); + await service.createLocation(spec, false, mockOptions); expect(fakeLocationService.createLocation).toHaveBeenCalledWith( spec, false, + mockOptions, ); }); @@ -71,9 +75,7 @@ describe('AuthorizedLocationService', () => { const spec = { type: 'type', target: 'target' }; await expect(() => - service.createLocation(spec, false, { - authorizationToken: 'Bearer authtoken', - }), + service.createLocation(spec, false, mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -83,7 +85,7 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.listLocations({ authorizationToken: 'Bearer authtoken' }); + await service.listLocations(mockOptions); expect(fakeLocationService.listLocations).toHaveBeenCalled(); }); @@ -92,9 +94,7 @@ describe('AuthorizedLocationService', () => { mockDeny(); const service = createService(); - const locations = await service.listLocations({ - authorizationToken: 'Bearer authtoken', - }); + const locations = await service.listLocations(mockOptions); expect(locations).toEqual([]); }); @@ -105,11 +105,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.getLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.getLocation('id', mockOptions); - expect(fakeLocationService.getLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.getLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -117,7 +118,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.getLocation('id', { authorizationToken: 'Bearer authtoken' }), + service.getLocation('id', mockOptions), ).rejects.toThrow(NotFoundError); }); }); @@ -127,11 +128,12 @@ describe('AuthorizedLocationService', () => { mockAllow(); const service = createService(); - await service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }); + await service.deleteLocation('id', mockOptions); - expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith('id'); + expect(fakeLocationService.deleteLocation).toHaveBeenCalledWith( + 'id', + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -139,9 +141,7 @@ describe('AuthorizedLocationService', () => { const service = createService(); await expect(() => - service.deleteLocation('id', { - authorizationToken: 'Bearer authtoken', - }), + service.deleteLocation('id', mockOptions), ).rejects.toThrow(NotAllowedError); }); }); @@ -153,16 +153,17 @@ describe('AuthorizedLocationService', () => { await service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { - authorizationToken: 'Bearer authtoken', - }, + mockOptions, ); - expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith({ - kind: 'c', - namespace: 'ns', - name: 'n', - }); + expect(fakeLocationService.getLocationByEntity).toHaveBeenCalledWith( + { + kind: 'c', + namespace: 'ns', + name: 'n', + }, + mockOptions, + ); }); it('throws error on DENY', async () => { @@ -172,7 +173,7 @@ describe('AuthorizedLocationService', () => { await expect(() => service.getLocationByEntity( { kind: 'c', namespace: 'ns', name: 'n' }, - { authorizationToken: 'Bearer authtoken' }, + mockOptions, ), ).rejects.toThrow(NotFoundError); }); diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 0b9d50b20e..3eb3eede12 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -22,23 +22,24 @@ import { catalogLocationDeletePermission, catalogLocationReadPermission, } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { LocationInput, LocationService } from './types'; +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; export class AuthorizedLocationService implements LocationService { constructor( private readonly locationService: LocationService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async createLocation( spec: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; @@ -48,7 +49,7 @@ export class AuthorizedLocationService implements LocationService { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationCreatePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -56,16 +57,16 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.createLocation(spec, dryRun); + return this.locationService.createLocation(spec, dryRun, options); } - async listLocations(options?: { - authorizationToken?: string; + async listLocations(options: { + credentials: BackstageCredentials; }): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -73,17 +74,17 @@ export class AuthorizedLocationService implements LocationService { return []; } - return this.locationService.listLocations(); + return this.locationService.listLocations(options); } async getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -91,17 +92,17 @@ export class AuthorizedLocationService implements LocationService { throw new NotFoundError(`Found no location with ID ${id}`); } - return this.locationService.getLocation(id); + return this.locationService.getLocation(id, options); } async deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationDeletePermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; @@ -109,23 +110,23 @@ export class AuthorizedLocationService implements LocationService { throw new NotAllowedError(); } - return this.locationService.deleteLocation(id); + return this.locationService.deleteLocation(id, options); } async getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string | undefined } | undefined, + options: { credentials: BackstageCredentials }, ): Promise { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationReadPermission }], - { token: options?.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizationResponse.result === AuthorizeResult.DENY) { throw new NotFoundError(); } - return this.locationService.getLocationByEntity(entityRef); + return this.locationService.getLocationByEntity(entityRef, options); } } diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts index f37bd0ec49..ba9ef2db73 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.test.ts @@ -18,6 +18,7 @@ import { NotAllowedError } from '@backstage/errors'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { AuthorizedRefreshService } from './AuthorizedRefreshService'; +import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedRefreshService', () => { const refreshService = { @@ -46,7 +47,7 @@ describe('AuthorizedRefreshService', () => { await expect(() => authorizedService.refresh({ entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }), ).rejects.toThrow(NotAllowedError); }); @@ -64,7 +65,7 @@ describe('AuthorizedRefreshService', () => { const options = { entityRef: 'some entity ref', - authorizationToken: 'some auth token', + credentials: mockCredentials.none(), }; await authorizedService.refresh(options); diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index cbc728750a..a5a2491a04 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -16,16 +16,14 @@ import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha'; -import { - AuthorizeResult, - PermissionEvaluator, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { RefreshOptions, RefreshService } from './types'; +import { PermissionsService } from '@backstage/backend-plugin-api'; export class AuthorizedRefreshService implements RefreshService { constructor( private readonly service: RefreshService, - private readonly permissionApi: PermissionEvaluator, + private readonly permissionApi: PermissionsService, ) {} async refresh(options: RefreshOptions) { @@ -37,7 +35,7 @@ export class AuthorizedRefreshService implements RefreshService { resourceRef: options.entityRef, }, ], - { token: options.authorizationToken }, + { credentials: options.credentials }, ) )[0]; if (authorizeDecision.result !== AuthorizeResult.ALLOW) { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 0abaf2ec0a..7fcee231fd 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; +import { + PluginDatabaseManager, + HostDiscovery, + UrlReader, + createLegacyAuthAdapters, +} from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { DefaultNamespaceEntityPolicy, @@ -84,7 +89,6 @@ import { permissionRules as catalogPermissionRules } from '../permissions/rules' import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionAuthorizer, - PermissionEvaluator, toPermissionEvaluator, } from '@backstage/plugin-permission-common'; import { @@ -102,6 +106,12 @@ import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase'; import { EventBroker } from '@backstage/plugin-events-node'; import { durationToMilliseconds } from '@backstage/types'; +import { + DiscoveryService, + AuthService, + HttpAuthService, + PermissionsService, +} from '@backstage/backend-plugin-api'; /** * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. @@ -118,8 +128,11 @@ export type CatalogEnvironment = { database: PluginDatabaseManager; config: Config; reader: UrlReader; - permissions: PermissionEvaluator | PermissionAuthorizer; + permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; + discovery?: DiscoveryService; + auth?: AuthService; + httpAuth?: HttpAuthService; }; /** @@ -438,7 +451,19 @@ export class CatalogBuilder { processingEngine: CatalogProcessingEngine; router: Router; }> { - const { config, database, logger, permissions, scheduler } = this.env; + const { + config, + database, + logger, + permissions, + scheduler, + discovery = HostDiscovery.fromConfig(config), + } = this.env; + + const { auth, httpAuth } = createLegacyAuthAdapters({ + ...this.env, + discovery, + }); const policy = this.buildEntityPolicy(); const processors = this.buildProcessors(); @@ -486,25 +511,26 @@ export class CatalogBuilder { stitcher, }); - let permissionEvaluator: PermissionEvaluator; + let permissionsService: PermissionsService; if ('authorizeConditional' in permissions) { - permissionEvaluator = permissions as PermissionEvaluator; + permissionsService = permissions as PermissionsService; } else { logger.warn( 'PermissionAuthorizer is deprecated. Please use an instance of PermissionEvaluator instead of PermissionAuthorizer in PluginEnvironment#permissions', ); - permissionEvaluator = toPermissionEvaluator(permissions); + permissionsService = toPermissionEvaluator(permissions); } const entitiesCatalog = new AuthorizedEntitiesCatalog( unauthorizedEntitiesCatalog, - permissionEvaluator, + permissionsService, createConditionTransformer(this.permissionRules), ); const permissionIntegrationRouter = createPermissionIntegrationRouter({ resourceType: RESOURCE_TYPE_CATALOG_ENTITY, getResources: async (resourceRefs: string[]) => { const { entities } = await unauthorizedEntitiesCatalog.entities({ + credentials: await auth.getOwnServiceCredentials(), filter: { anyOf: resourceRefs.map(resourceRef => { const { kind, namespace, name } = parseEntityRef(resourceRef); @@ -558,12 +584,13 @@ export class CatalogBuilder { new DefaultLocationService(locationStore, orchestrator, { allowedLocationTypes: this.allowedLocationType, }), - permissionEvaluator, + permissionsService, ); const refreshService = new AuthorizedRefreshService( new DefaultRefreshService({ database: catalogDatabase }), - permissionEvaluator, + permissionsService, ); + const router = await createRouter({ entitiesCatalog, locationAnalyzer, @@ -573,6 +600,8 @@ export class CatalogBuilder { logger, config, permissionIntegrationRouter, + auth, + httpAuth, }); await connectEntityProviders(providerDatabase, entityProviders); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 255df84d58..87e4316c11 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -178,6 +178,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, lifecycle: coreServices.lifecycle, scheduler: coreServices.scheduler, + discovery: coreServices.discovery, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, async init({ logger, @@ -188,6 +191,9 @@ export const catalogPlugin = createBackendPlugin({ httpRouter, lifecycle, scheduler, + discovery, + auth, + httpAuth, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -197,6 +203,9 @@ export const catalogPlugin = createBackendPlugin({ database, scheduler, logger: winstonLogger, + discovery, + auth, + httpAuth, }); if (processingExtensions.onProcessingErrorHandler) { builder.subscribe({ diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index fe7d57c89a..4d8dc7c7a4 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Knex } from 'knex'; import { v4 as uuid, v4 } from 'uuid'; @@ -304,8 +308,10 @@ describe('DefaultEntitiesCatalog', () => { const testFilter = { key: 'spec.test', }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity2); @@ -343,8 +349,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'spec.test', }, }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities[0]).toEqual(entity1); @@ -406,7 +414,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['red'], }, }; - const request = { + const { entities } = await catalog.entities({ filter: { allOf: [ testFilter1, @@ -415,8 +423,8 @@ describe('DefaultEntitiesCatalog', () => { }, ], }, - }; - const { entities } = await catalog.entities(request); + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(2); expect(entities).toContainEqual(entity2); @@ -455,14 +463,15 @@ describe('DefaultEntitiesCatalog', () => { const testFilter2 = { key: 'metadata.desc', }; - const request = { + const { entities } = await catalog.entities({ filter: { not: { allOf: [testFilter1, testFilter2], }, }, - }; - const { entities } = await catalog.entities(request); + + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(1); expect(entities).toContainEqual(entity1); @@ -498,8 +507,10 @@ describe('DefaultEntitiesCatalog', () => { key: 'kind', values: [], }; - const request = { filter: testFilter }; - const { entities } = await catalog.entities(request); + const { entities } = await catalog.entities({ + filter: testFilter, + credentials: mockCredentials.none(), + }); expect(entities.length).toBe(0); }, @@ -603,9 +614,11 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - function f(request: EntitiesRequest): Promise { + function f( + request: Omit, + ): Promise { return catalog - .entities(request) + .entities({ ...request, credentials: mockCredentials.none() }) .then(response => response.entities.map(e => e.metadata.name)); } @@ -701,6 +714,7 @@ describe('DefaultEntitiesCatalog', () => { 'k:default/does-not-exist', 'k:default/two', ], + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -749,6 +763,7 @@ describe('DefaultEntitiesCatalog', () => { const { items } = await catalog.entitiesBatch({ entityRefs: ['k:default/two', 'k:default/one'], filter: { key: 'spec.owner', values: ['me'] }, + credentials: mockCredentials.none(), }); expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ @@ -804,6 +819,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -815,6 +831,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -826,6 +843,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -837,6 +855,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -848,6 +867,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('A'), entityFrom('B')]); @@ -859,6 +879,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('C'), entityFrom('D')]); @@ -870,6 +891,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('E'), entityFrom('F')]); @@ -881,6 +903,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -896,6 +919,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('G')]); @@ -949,6 +973,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit, orderFields: [{ field: 'metadata.name', order: 'desc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -960,6 +985,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -971,6 +997,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -982,6 +1009,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); @@ -994,6 +1022,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toEqual([entityFrom('G'), entityFrom('F')]); @@ -1005,6 +1034,7 @@ describe('DefaultEntitiesCatalog', () => { const request6: QueryEntitiesCursorRequest = { cursor: response5.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response6 = await catalog.queryEntities(request6); expect(response6.items).toEqual([entityFrom('E'), entityFrom('D')]); @@ -1016,6 +1046,7 @@ describe('DefaultEntitiesCatalog', () => { const request7: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response7 = await catalog.queryEntities(request7); expect(response7.items).toEqual([entityFrom('C'), entityFrom('B')]); @@ -1027,6 +1058,7 @@ describe('DefaultEntitiesCatalog', () => { const request7bis: QueryEntitiesCursorRequest = { cursor: response6.pageInfo.nextCursor!, limit: limit + 1, + credentials: mockCredentials.none(), }; const response7bis = await catalog.queryEntities(request7bis); expect(response7bis.items).toEqual([ @@ -1042,6 +1074,7 @@ describe('DefaultEntitiesCatalog', () => { const request8: QueryEntitiesCursorRequest = { cursor: response7.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response8 = await catalog.queryEntities(request8); expect(response8.items).toEqual([entityFrom('A')]); @@ -1094,6 +1127,7 @@ describe('DefaultEntitiesCatalog', () => { orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'cAt ' }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1152,6 +1186,7 @@ describe('DefaultEntitiesCatalog', () => { filter, limit: 100, fullTextFilter: { term: 'cAt ', fields: ['metadata.title'] }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response.items).toEqual([ @@ -1177,6 +1212,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('4', { uid: 'id4', title: 'dogcat' }), @@ -1187,6 +1223,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1251,6 +1288,7 @@ describe('DefaultEntitiesCatalog', () => { term: 'KiNg ', fields: ['metadata.title', 'metadata.name'], }, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); @@ -1278,6 +1316,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponseNext = await catalog.queryEntities({ cursor: paginatedResponse.pageInfo.nextCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponseNext.items).toEqual([ entityFrom('NotACatKing', { uid: 'id2', title: 'atcatss' }), @@ -1289,6 +1328,7 @@ describe('DefaultEntitiesCatalog', () => { const paginatedResponsePrev = await catalog.queryEntities({ cursor: paginatedResponseNext.pageInfo.prevCursor!, + credentials: mockCredentials.none(), }); expect(paginatedResponsePrev).toMatchObject(paginatedResponse); }, @@ -1319,6 +1359,7 @@ describe('DefaultEntitiesCatalog', () => { const request: QueryEntitiesInitialRequest = { limit: 0, + credentials: mockCredentials.none(), }; const response = await catalog.queryEntities(request); expect(response).toEqual({ totalItems: 20, items: [], pageInfo: {} }); @@ -1351,6 +1392,7 @@ describe('DefaultEntitiesCatalog', () => { const request1: QueryEntitiesInitialRequest = { limit, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1365,6 +1407,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1379,6 +1422,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toEqual([entityFrom('CC'), entityFrom('DD')]); @@ -1390,6 +1434,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1404,6 +1449,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1471,6 +1517,7 @@ describe('DefaultEntitiesCatalog', () => { values: ['included'], }, orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1485,6 +1532,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1528,6 +1576,7 @@ describe('DefaultEntitiesCatalog', () => { // initial request const request1: QueryEntitiesInitialRequest = { limit, + credentials: mockCredentials.none(), }; const response1 = await catalog.queryEntities(request1); expect(response1.items).toMatchObject([ @@ -1542,6 +1591,7 @@ describe('DefaultEntitiesCatalog', () => { const request2: QueryEntitiesCursorRequest = { cursor: response1.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response2 = await catalog.queryEntities(request2); expect(response2.items).toMatchObject([ @@ -1556,6 +1606,7 @@ describe('DefaultEntitiesCatalog', () => { const request3: QueryEntitiesCursorRequest = { cursor: response2.pageInfo.nextCursor!, limit, + credentials: mockCredentials.none(), }; const response3 = await catalog.queryEntities(request3); expect(response3.items).toMatchObject([ @@ -1570,6 +1621,7 @@ describe('DefaultEntitiesCatalog', () => { const request4: QueryEntitiesCursorRequest = { cursor: response3.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response4 = await catalog.queryEntities(request4); expect(response4.items).toMatchObject([ @@ -1584,6 +1636,7 @@ describe('DefaultEntitiesCatalog', () => { const request5: QueryEntitiesCursorRequest = { cursor: response4.pageInfo.prevCursor!, limit, + credentials: mockCredentials.none(), }; const response5 = await catalog.queryEntities(request5); expect(response5.items).toMatchObject([ @@ -1719,7 +1772,12 @@ describe('DefaultEntitiesCatalog', () => { stitcher, }); - await expect(catalog.facets({ facets: ['kind'] })).resolves.toEqual({ + await expect( + catalog.facets({ + facets: ['kind'], + credentials: mockCredentials.none(), + }), + ).resolves.toEqual({ facets: { kind: [ { value: 'k', count: 2 }, @@ -1732,6 +1790,7 @@ describe('DefaultEntitiesCatalog', () => { catalog.facets({ facets: ['kind'], filter: { not: { key: 'metadata.name', values: ['two'] } }, + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1775,6 +1834,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.annotations.a.b/c.d', 'metadata.labels.e.f/g.h'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { @@ -1823,6 +1883,7 @@ describe('DefaultEntitiesCatalog', () => { await expect( catalog.facets({ facets: ['metadata.tags'], + credentials: mockCredentials.none(), }), ).resolves.toEqual({ facets: { diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index e1b775cce6..9ca9ae0a5f 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -15,7 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockCredentials, +} from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { createHash } from 'crypto'; import { Knex } from 'knex'; @@ -220,6 +224,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -273,6 +278,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'api:default/myapi', + credentials: mockCredentials.none(), }); await expect(waitForRefresh(knex, 'api:default/myapi')).resolves.toBe( @@ -324,6 +330,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( @@ -334,6 +341,7 @@ describe('DefaultRefreshService', () => { await refreshService.refresh({ entityRef: 'component:default/mycomp', + credentials: mockCredentials.none(), }); await expect( diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 3eb00c5bd6..5177f3b42b 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -41,6 +41,7 @@ import { z } from 'zod'; import { decodeCursor, encodeCursor } from './util'; import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils'; import { Server } from 'http'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; @@ -75,6 +76,8 @@ describe('createRouter readonly disabled', () => { refreshService, config: new ConfigReader(undefined), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = wrapInOpenApiTestServer(express().use(router)); }); @@ -88,15 +91,30 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/refresh') .set('Content-Type', 'application/json') - .set('authorization', 'Bearer someauthtoken') .send({ entityRef: 'Component/default:foo' }); expect(response.status).toBe(200); expect(refreshService.refresh).toHaveBeenCalledWith({ entityRef: 'Component/default:foo', - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), + }); + }); + + it('should support passing the token in the request body for backwards compatibility', async () => { + const response = await request(app) + .post('/refresh') + .set('Content-Type', 'application/json') + .send({ + entityRef: 'Component/default:foo', + authorizationToken: mockCredentials.user.token('user:default/other'), + }); + expect(response.status).toBe(200); + expect(refreshService.refresh).toHaveBeenCalledWith({ + entityRef: 'Component/default:foo', + credentials: mockCredentials.user('user:default/other'), }); }); }); + describe('GET /entities', () => { it('happy path: lists entities', async () => { const entities: Entity[] = [ @@ -137,6 +155,7 @@ describe('createRouter readonly disabled', () => { { allOf: [{ key: 'c', values: ['4'] }] }, ], }, + credentials: mockCredentials.user(), }); }); }); @@ -196,6 +215,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -235,6 +255,7 @@ describe('createRouter readonly disabled', () => { fields: undefined, term: '', }, + credentials: mockCredentials.user(), }); }); @@ -257,6 +278,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -291,6 +313,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({ cursor, + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ @@ -370,6 +393,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -386,6 +410,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith({ filter: basicEntityFilter({ 'metadata.uid': 'zzz' }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); @@ -416,6 +441,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'ns', 'metadata.name': 'n', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); @@ -436,6 +462,7 @@ describe('createRouter readonly disabled', () => { 'metadata.namespace': 'd', 'metadata.name': 'c', }), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); @@ -446,13 +473,10 @@ describe('createRouter readonly disabled', () => { it('can remove', async () => { entitiesCatalog.removeEntityByUid.mockResolvedValue(undefined); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -462,13 +486,10 @@ describe('createRouter readonly disabled', () => { new NotFoundError('nope'), ); - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(404); }); @@ -518,6 +539,7 @@ describe('createRouter readonly disabled', () => { expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledWith({ entityRefs: [entityRef], fields: expect.any(Function), + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual({ items: [entity] }); @@ -531,13 +553,10 @@ describe('createRouter readonly disabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); expect(response.body).toEqual([ @@ -555,13 +574,10 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -582,7 +598,7 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -602,12 +618,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, false, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -630,12 +646,12 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -650,13 +666,10 @@ describe('createRouter readonly disabled', () => { it('deletes the location', async () => { locationService.deleteLocation.mockResolvedValueOnce(undefined); - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).toHaveBeenCalledTimes(1); expect(locationService.deleteLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); @@ -672,15 +685,12 @@ describe('createRouter readonly disabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -837,6 +847,8 @@ describe('createRouter readonly enabled', () => { }, }), permissionIntegrationRouter: express.Router(), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); @@ -866,13 +878,10 @@ describe('createRouter readonly enabled', () => { describe('DELETE /entities/by-uid/:uid', () => { // this delete is allowed as there is no other way to remove entities it('is allowed', async () => { - const response = await request(app) - .delete('/entities/by-uid/apa') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/entities/by-uid/apa'); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(204); }); @@ -885,13 +894,10 @@ describe('createRouter readonly enabled', () => { ]; locationService.listLocations.mockResolvedValueOnce(locations); - const response = await request(app) - .get('/locations') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations'); expect(locationService.listLocations).toHaveBeenCalledTimes(1); expect(locationService.listLocations).toHaveBeenCalledWith({ - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -910,13 +916,10 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocation.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/foo'); expect(locationService.getLocation).toHaveBeenCalledTimes(1); expect(locationService.getLocation).toHaveBeenCalledWith('foo', { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(200); @@ -937,7 +940,7 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).not.toHaveBeenCalled(); @@ -958,12 +961,12 @@ describe('createRouter readonly enabled', () => { const response = await request(app) .post('/locations?dryRun=true') - .set('authorization', 'Bearer someauthtoken') + .send(spec); expect(locationService.createLocation).toHaveBeenCalledTimes(1); expect(locationService.createLocation).toHaveBeenCalledWith(spec, true, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }); expect(response.status).toEqual(201); expect(response.body).toEqual( @@ -976,10 +979,7 @@ describe('createRouter readonly enabled', () => { describe('DELETE /locations', () => { it('is not allowed', async () => { - const response = await request(app) - .delete('/locations/foo') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).delete('/locations/foo'); expect(locationService.deleteLocation).not.toHaveBeenCalled(); expect(response.status).toEqual(403); }); @@ -994,15 +994,12 @@ describe('createRouter readonly enabled', () => { }; locationService.getLocationByEntity.mockResolvedValueOnce(location); - const response = await request(app) - .get('/locations/by-entity/c/ns/n') - .set('authorization', 'Bearer someauthtoken'); - + const response = await request(app).get('/locations/by-entity/c/ns/n'); expect(locationService.getLocationByEntity).toHaveBeenCalledTimes(1); expect(locationService.getLocationByEntity).toHaveBeenCalledWith( { kind: 'c', namespace: 'ns', name: 'n' }, { - authorizationToken: 'someauthtoken', + credentials: mockCredentials.user(), }, ); @@ -1065,6 +1062,8 @@ describe('NextRouter permissioning', () => { ), ), }), + auth: mockServices.auth(), + httpAuth: mockServices.httpAuth(), }); app = express().use(router); }); diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 2522966408..922782d75a 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -41,7 +41,7 @@ import { } from './request'; import { parseEntityFacetParams } from './request/parseEntityFacetParams'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; -import { LocationService, RefreshOptions, RefreshService } from './types'; +import { LocationService, RefreshService } from './types'; import { disallowReadonlyMode, encodeCursor, @@ -50,8 +50,8 @@ import { } from './util'; import { createOpenApiRouter } from '../schema/openapi.generated'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { parseEntityPaginationParams } from './request/parseEntityPaginationParams'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; /** * Options used by {@link createRouter}. @@ -68,6 +68,8 @@ export interface RouterOptions { logger: Logger; config: Config; permissionIntegrationRouter?: express.Router; + auth: AuthService; + httpAuth: HttpAuthService; } /** @@ -94,6 +96,8 @@ export async function createRouter( config, logger, permissionIntegrationRouter, + auth, + httpAuth, } = options; const readonlyEnabled = @@ -104,12 +108,16 @@ export async function createRouter( if (refreshService) { router.post('/refresh', async (req, res) => { - const refreshOptions: RefreshOptions = req.body; - refreshOptions.authorizationToken = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); + const { authorizationToken, ...restBody } = req.body; - await refreshService.refresh(refreshOptions); + const credentials = authorizationToken + ? await auth.authenticate(authorizationToken) + : await httpAuth.credentials(req); + + await refreshService.refresh({ + ...restBody, + credentials, + }); res.status(200).end(); }); } @@ -126,9 +134,7 @@ export async function createRouter( fields: parseEntityTransformParams(req.query), order: parseEntityOrderParams(req.query), pagination: parseEntityPaginationParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); // Add a Link header to the next page @@ -147,9 +153,7 @@ export async function createRouter( await entitiesCatalog.queryEntities({ limit: req.query.limit, ...parseQueryEntitiesParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.json({ @@ -169,9 +173,7 @@ export async function createRouter( const { uid } = req.params; const { entities } = await entitiesCatalog.entities({ filter: basicEntityFilter({ 'metadata.uid': uid }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError(`No entity with uid ${uid}`); @@ -181,9 +183,7 @@ export async function createRouter( .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -195,9 +195,7 @@ export async function createRouter( 'metadata.namespace': namespace, 'metadata.name': name, }), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); if (!entities.length) { throw new NotFoundError( @@ -212,22 +210,17 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entityRef = stringifyEntityRef({ kind, namespace, name }); const response = await entitiesCatalog.entityAncestry(entityRef, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }, ) .post('/entities/by-refs', async (req, res) => { const request = entitiesBatchRequest(req); - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); const response = await entitiesCatalog.entitiesBatch({ entityRefs: request.entityRefs, fields: parseEntityTransformParams(req.query, request.fields), - authorizationToken: token, + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }) @@ -235,9 +228,7 @@ export async function createRouter( const response = await entitiesCatalog.facets({ filter: parseEntityFilterParams(req.query), facets: parseEntityFacetParams(req.query), - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(response); }); @@ -256,17 +247,13 @@ export async function createRouter( } const output = await locationService.createLocation(location, dryRun, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(201).json(output); }) .get('/locations', async (req, res) => { const locations = await locationService.listLocations({ - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(locations.map(l => ({ data: l }))); }) @@ -274,9 +261,7 @@ export async function createRouter( .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationService.getLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(200).json(output); }) @@ -285,9 +270,7 @@ export async function createRouter( const { id } = req.params; await locationService.deleteLocation(id, { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), + credentials: await httpAuth.credentials(req), }); res.status(204).end(); }) @@ -295,11 +278,7 @@ export async function createRouter( const { kind, namespace, name } = req.params; const output = await locationService.getLocationByEntity( { kind, namespace, name }, - { - authorizationToken: getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ), - }, + { credentials: await httpAuth.credentials(req) }, ); res.status(200).json(output); }); diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts index b1d5e8dd1f..919e97a1f7 100644 --- a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts @@ -28,12 +28,12 @@ import { internal } from '@backstage/backend-openapi-utils'; export function parseQueryEntitiesParams( params: internal.QuerySchema, -): Omit { +): Omit { const fields = parseEntityTransformParams(params); if (params.cursor) { const decodedCursor = decodeCursor(params.cursor); - const response: Omit = { + const response: Omit = { cursor: decodedCursor, fields, }; @@ -43,7 +43,7 @@ export function parseQueryEntitiesParams( const filter = parseEntityFilterParams(params); const orderFields = parseEntityOrderFieldParams(params); - const response: Omit = { + const response: Omit = { fields, filter, orderFields, diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 878afe5662..30470e5af0 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -16,6 +16,7 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { Location } from '@backstage/catalog-client'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; /** * Holds the information required to create a new location in the catalog location store. @@ -35,22 +36,24 @@ export interface LocationService { createLocation( location: LocationInput, dryRun: boolean, - options?: { - authorizationToken?: string; + options: { + credentials: BackstageCredentials; }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }>; - listLocations(options?: { authorizationToken?: string }): Promise; + listLocations(options: { + credentials: BackstageCredentials; + }): Promise; getLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; deleteLocation( id: string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; getLocationByEntity( entityRef: CompoundEntityRef | string, - options?: { authorizationToken?: string }, + options: { credentials: BackstageCredentials }, ): Promise; } @@ -62,7 +65,7 @@ export interface LocationService { export type RefreshOptions = { /** The reference to a single entity that should be refreshed */ entityRef: string; - authorizationToken?: string; + credentials: BackstageCredentials; }; /** From 56969b6e550ff48c5c9cefab40958b50f8dbb99e Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 18:30:14 +0100 Subject: [PATCH 328/483] feat(events): add new events service Signed-off-by: Patrick Jungermann --- .changeset/breezy-cycles-count.md | 39 ++++++ .../events-backend-test-utils/api-report.md | 20 +++- .../src/deprecated.ts | 19 +++ .../events-backend-test-utils/src/index.ts | 1 + .../src/testUtils/TestEventBroker.ts | 5 +- .../src/testUtils/TestEventPublisher.ts | 5 +- .../src/testUtils/TestEventSubscriber.ts | 5 +- .../src/testUtils/TestEventsService.ts | 48 ++++++++ .../src/testUtils/index.ts | 4 +- plugins/events-backend/api-report.md | 9 +- plugins/events-backend/src/deprecated.ts | 18 +++ plugins/events-backend/src/index.ts | 3 +- .../src/service/DefaultEventBroker.test.ts | 18 +-- .../src/service/DefaultEventBroker.ts | 50 ++++---- .../src/service/EventsBackend.ts | 1 + plugins/events-node/api-report-alpha.md | 6 +- plugins/events-node/api-report.md | 39 +++++- plugins/events-node/package.json | 1 + .../src/api/DefaultEventsService.test.ts | 111 ++++++++++++++++++ .../src/api/DefaultEventsService.ts | 104 ++++++++++++++++ plugins/events-node/src/api/EventBroker.ts | 1 + plugins/events-node/src/api/EventPublisher.ts | 4 + .../events-node/src/api/EventSubscriber.ts | 4 + plugins/events-node/src/api/EventsService.ts | 57 +++++++++ plugins/events-node/src/api/index.ts | 9 +- plugins/events-node/src/deprecated.ts | 19 +++ plugins/events-node/src/extensions.ts | 9 ++ plugins/events-node/src/index.ts | 2 + plugins/events-node/src/service.ts | 47 ++++++++ yarn.lock | 1 + 30 files changed, 600 insertions(+), 59 deletions(-) create mode 100644 .changeset/breezy-cycles-count.md create mode 100644 plugins/events-backend-test-utils/src/deprecated.ts create mode 100644 plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts create mode 100644 plugins/events-backend/src/deprecated.ts create mode 100644 plugins/events-node/src/api/DefaultEventsService.test.ts create mode 100644 plugins/events-node/src/api/DefaultEventsService.ts create mode 100644 plugins/events-node/src/api/EventsService.ts create mode 100644 plugins/events-node/src/deprecated.ts create mode 100644 plugins/events-node/src/service.ts diff --git a/.changeset/breezy-cycles-count.md b/.changeset/breezy-cycles-count.md new file mode 100644 index 0000000000..81998d9e8b --- /dev/null +++ b/.changeset/breezy-cycles-count.md @@ -0,0 +1,39 @@ +--- +'@backstage/plugin-events-backend-test-utils': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-node': patch +--- + +Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + +**Summary:** + +- new: + `EventsService`, `eventsServiceRef`, `TestEventsService` +- deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + +Add the `eventsServiceRef` as dependency to your backend plugins +or backend plugin modules. + +**Details:** + +The previous implementation using the `EventsExtensionPoint` was added in the early stages +of the new backend system and does not respect the plugin isolation. +This made it not compatible anymore with the new backend system. + +Additionally, the previous interfaces had some room for simplification, +supporting less exposure of internal concerns as well. + +Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. +The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. +Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, +and used internally. There is no need to expose anything of that at their own interfaces. + +Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable +(by other plugins or their modules) anyway. + +The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. +Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. diff --git a/plugins/events-backend-test-utils/api-report.md b/plugins/events-backend-test-utils/api-report.md index 46131c4244..9630c3d4e2 100644 --- a/plugins/events-backend-test-utils/api-report.md +++ b/plugins/events-backend-test-utils/api-report.md @@ -6,9 +6,11 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; +import { EventsServiceSubscribeOptions } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventBroker implements EventBroker { // (undocumented) publish(params: EventParams): Promise; @@ -22,7 +24,7 @@ export class TestEventBroker implements EventBroker { readonly subscribed: EventSubscriber[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventPublisher implements EventPublisher { // (undocumented) get eventBroker(): EventBroker | undefined; @@ -31,6 +33,20 @@ export class TestEventPublisher implements EventPublisher { } // @public (undocumented) +export class TestEventsService implements EventsService { + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + get published(): EventParams[]; + // (undocumented) + reset(): void; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; + // (undocumented) + get subscribed(): EventsServiceSubscribeOptions[]; +} + +// @public @deprecated (undocumented) export class TestEventSubscriber implements EventSubscriber { constructor(name: string, topics: string[]); // (undocumented) diff --git a/plugins/events-backend-test-utils/src/deprecated.ts b/plugins/events-backend-test-utils/src/deprecated.ts new file mode 100644 index 0000000000..15072dcfb4 --- /dev/null +++ b/plugins/events-backend-test-utils/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export { TestEventBroker } from './testUtils/TestEventBroker'; +export { TestEventPublisher } from './testUtils/TestEventPublisher'; +export { TestEventSubscriber } from './testUtils/TestEventSubscriber'; diff --git a/plugins/events-backend-test-utils/src/index.ts b/plugins/events-backend-test-utils/src/index.ts index efba5be0d0..da090488fc 100644 --- a/plugins/events-backend-test-utils/src/index.ts +++ b/plugins/events-backend-test-utils/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export * from './deprecated'; export * from './testUtils'; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts index c697a6506f..78556cfc63 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts @@ -20,7 +20,10 @@ import { EventSubscriber, } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated use `TestEventsService` instead + */ export class TestEventBroker implements EventBroker { readonly published: EventParams[] = []; readonly subscribed: EventSubscriber[] = []; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts index c1b2038afb..51bad11278 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts @@ -16,7 +16,10 @@ import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventPublisher` was replaced by `EventsService.publish` + */ export class TestEventPublisher implements EventPublisher { #eventBroker?: EventBroker; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts index ef5758b804..3db9023a9b 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts @@ -16,7 +16,10 @@ import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventSubscriber` was replaced by `EventsService.subscribe`. + */ export class TestEventSubscriber implements EventSubscriber { readonly name: string; readonly topics: string[]; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts new file mode 100644 index 0000000000..c87072711c --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventsService.ts @@ -0,0 +1,48 @@ +/* + * 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 { + EventParams, + EventsService, + EventsServiceSubscribeOptions, +} from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventsService implements EventsService { + #published: EventParams[] = []; + #subscribed: EventsServiceSubscribeOptions[] = []; + + async publish(params: EventParams): Promise { + this.#published.push(params); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + this.#subscribed.push(options); + } + + get published(): EventParams[] { + return this.#published; + } + + get subscribed(): EventsServiceSubscribeOptions[] { + return this.#subscribed; + } + + reset(): void { + this.#published = []; + this.#subscribed = []; + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/index.ts b/plugins/events-backend-test-utils/src/testUtils/index.ts index a571ba3075..d9eb544628 100644 --- a/plugins/events-backend-test-utils/src/testUtils/index.ts +++ b/plugins/events-backend-test-utils/src/testUtils/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { TestEventBroker } from './TestEventBroker'; -export { TestEventPublisher } from './TestEventPublisher'; -export { TestEventSubscriber } from './TestEventSubscriber'; +export { TestEventsService } from './TestEventsService'; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index aeb6f9363d..9fffe719ba 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -7,14 +7,17 @@ import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; -// @public +// @public @deprecated export class DefaultEventBroker implements EventBroker { - constructor(logger: Logger); + // @deprecated + constructor(logger: LoggerService, events?: EventsService); // (undocumented) publish(params: EventParams): Promise; // (undocumented) @@ -23,7 +26,7 @@ export class DefaultEventBroker implements EventBroker { ): void; } -// @public +// @public @deprecated export class EventsBackend { constructor(logger: Logger); // (undocumented) diff --git a/plugins/events-backend/src/deprecated.ts b/plugins/events-backend/src/deprecated.ts new file mode 100644 index 0000000000..cce853b2af --- /dev/null +++ b/plugins/events-backend/src/deprecated.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { EventsBackend } from './service/EventsBackend'; +export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts index be173b677c..63dfa5d252 100644 --- a/plugins/events-backend/src/index.ts +++ b/plugins/events-backend/src/index.ts @@ -20,6 +20,5 @@ * @packageDocumentation */ -export { EventsBackend } from './service/EventsBackend'; +export * from './deprecated'; export { HttpPostIngressEventPublisher } from './service/http'; -export { DefaultEventBroker } from './service/DefaultEventBroker'; diff --git a/plugins/events-backend/src/service/DefaultEventBroker.test.ts b/plugins/events-backend/src/service/DefaultEventBroker.test.ts index 99e5f6f72d..5317251726 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.test.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.test.ts @@ -85,15 +85,15 @@ describe('DefaultEventBroker', () => { } })(); - const errorSpy = jest.spyOn(logger, 'error'); + const warnSpy = jest.spyOn(logger, 'warn'); const eventBroker = new DefaultEventBroker(logger); eventBroker.subscribe(subscriber1); await eventBroker.publish({ topic, eventPayload: '1' }); - expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 1'), ); @@ -101,13 +101,13 @@ describe('DefaultEventBroker', () => { await eventBroker.publish({ topic, eventPayload: '2' }); // With two subscribers we should not halt on the first error but call all subscribers - expect(errorSpy).toHaveBeenCalledTimes(3); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber1" failed to process event', + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber1" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); - expect(errorSpy).toHaveBeenCalledWith( - 'Subscriber "Subscriber2" failed to process event', + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "Subscriber2" failed to process event for topic "testTopic"', new Error('NOPE 2'), ); }); diff --git a/plugins/events-backend/src/service/DefaultEventBroker.ts b/plugins/events-backend/src/service/DefaultEventBroker.ts index c3824b3e7d..27523f3118 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { LoggerService } from '@backstage/backend-plugin-api'; import { + DefaultEventsService, EventBroker, EventParams, + EventsService, EventSubscriber, } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; /** * In process event broker which will pass the event to all registered subscribers @@ -27,44 +29,34 @@ import { Logger } from 'winston'; * Events will not be persisted in any form. * * @public + * @deprecated use `DefaultEventsService` from `@backstage/plugin-events-node` instead */ -// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) export class DefaultEventBroker implements EventBroker { - constructor(private readonly logger: Logger) {} + private readonly events: EventsService; - private readonly subscribers: { - [topic: string]: EventSubscriber[]; - } = {}; + /** + * + * @param logger - logger + * @param events - replacement that gets wrapped to support not yet migrated implementations. + * An instance can be passed (required for a mixed mode), otherwise a new instance gets created internally. + * @deprecated use `DefaultEventsService` directly instead + */ + constructor(logger: LoggerService, events?: EventsService) { + this.events = events ?? DefaultEventsService.create({ logger }); + } async publish(params: EventParams): Promise { - this.logger.debug( - `Event received: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - - const subscribed = this.subscribers[params.topic] ?? []; - await Promise.all( - subscribed.map(async subscriber => { - try { - await subscriber.onEvent(params); - } catch (error) { - this.logger.error( - `Subscriber "${subscriber.constructor.name}" failed to process event`, - error, - ); - } - }), - ); + return this.events.publish(params); } subscribe( ...subscribers: Array> ): void { - subscribers.flat().forEach(subscriber => { - subscriber.supportsEventTopics().forEach(topic => { - this.subscribers[topic] = this.subscribers[topic] ?? []; - this.subscribers[topic].push(subscriber); + subscribers.flat().forEach(async subscriber => { + await this.events.subscribe({ + id: subscriber.constructor.name, + topics: subscriber.supportsEventTopics(), + onEvent: subscriber.onEvent.bind(subscriber), }); }); } diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts index 4415b8703a..2c93663b46 100644 --- a/plugins/events-backend/src/service/EventsBackend.ts +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -26,6 +26,7 @@ import { DefaultEventBroker } from './DefaultEventBroker'; * A builder that helps wire up all component parts of the event management. * * @public + * @deprecated `EventBroker`, `EventPublisher`, and `EventSubscriber` got replaced by `EventsService` and its methods. */ export class EventsBackend { private eventBroker: EventBroker; diff --git a/plugins/events-node/api-report-alpha.md b/plugins/events-node/api-report-alpha.md index fd30f54d45..f61048ac94 100644 --- a/plugins/events-node/api-report-alpha.md +++ b/plugins/events-node/api-report-alpha.md @@ -13,15 +13,15 @@ import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; export interface EventsExtensionPoint { // (undocumented) addHttpPostIngress(options: HttpPostIngressOptions): void; - // (undocumented) + // @deprecated (undocumented) addPublishers( ...publishers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) addSubscribers( ...subscribers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): void; } diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index dfda48d97e..081c56549a 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -3,7 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + // @public +export class DefaultEventsService implements EventsService { + // (undocumented) + static create(options: { logger: LoggerService }): DefaultEventsService; + forPlugin(pluginId: string): EventsService; + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public @deprecated export interface EventBroker { publish(params: EventParams): Promise; subscribe( @@ -18,9 +32,9 @@ export interface EventParams { topic: string; } -// @public +// @public @deprecated export interface EventPublisher { - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } @@ -39,8 +53,29 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // @public +export interface EventsService { + publish(params: EventParams): Promise; + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +// @public (undocumented) +export type EventsServiceEventHandler = (params: EventParams) => Promise; + +// @public +export const eventsServiceRef: ServiceRef; + +// @public (undocumented) +export type EventsServiceSubscribeOptions = { + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +// @public @deprecated export interface EventSubscriber { + // @deprecated onEvent(params: EventParams): Promise; + // @deprecated supportsEventTopics(): string[]; } diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 58ddd50413..de794dfa45 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -45,6 +45,7 @@ "@backstage/backend-plugin-api": "workspace:^" }, "devDependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ diff --git a/plugins/events-node/src/api/DefaultEventsService.test.ts b/plugins/events-node/src/api/DefaultEventsService.test.ts new file mode 100644 index 0000000000..33df923892 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2022 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 { getVoidLogger } from '@backstage/backend-common'; +import { DefaultEventsService } from './DefaultEventsService'; +import { EventParams } from './EventParams'; + +const logger = getVoidLogger(); + +describe('DefaultEventsService', () => { + it('passes events to interested subscribers', async () => { + const events = DefaultEventsService.create({ logger }); + const eventsSubscriber1: EventParams[] = []; + const eventsSubscriber2: EventParams[] = []; + + await events.subscribe({ + id: 'subscriber1', + topics: ['topicA', 'topicB'], + onEvent: async event => { + eventsSubscriber1.push(event); + }, + }); + await events.subscribe({ + id: 'subscriber2', + topics: ['topicB', 'topicC'], + onEvent: async event => { + eventsSubscriber2.push(event); + }, + }); + await events.publish({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + await events.publish({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + await events.publish({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + await events.publish({ + topic: 'topicD', + eventPayload: { test: 'topicD' }, + }); + + expect(eventsSubscriber1).toEqual([ + { topic: 'topicA', eventPayload: { test: 'topicA' } }, + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + ]); + expect(eventsSubscriber2).toEqual([ + { topic: 'topicB', eventPayload: { test: 'topicB' } }, + { topic: 'topicC', eventPayload: { test: 'topicC' } }, + ]); + }); + + it('logs errors from subscribers', async () => { + const topic = 'testTopic'; + + const warnSpy = jest.spyOn(logger, 'warn'); + const events = DefaultEventsService.create({ logger }); + + await events.subscribe({ + id: 'subscriber1', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '1' }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 1'), + ); + + await events.subscribe({ + id: 'subscriber2', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await events.publish({ topic, eventPayload: '2' }); + + // With two subscribers we should not halt on the first error but call all subscribers + expect(warnSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + expect(warnSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber2" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + }); +}); diff --git a/plugins/events-node/src/api/DefaultEventsService.ts b/plugins/events-node/src/api/DefaultEventsService.ts new file mode 100644 index 0000000000..bb5c2a0ca8 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventsService.ts @@ -0,0 +1,104 @@ +/* + * 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { EventParams } from './EventParams'; +import { EventsService, EventsServiceSubscribeOptions } from './EventsService'; + +/** + * In-process event broker which will pass the event to all registered subscribers + * interested in it. + * Events will not be persisted in any form. + * Events will not be passed to subscribers at other instances of the same cluster. + * + * @public + */ +// TODO(pjungermann): add opentelemetry? (see plugins/catalog-backend/src/util/opentelemetry.ts, etc.) +export class DefaultEventsService implements EventsService { + private readonly subscribers = new Map< + string, + Omit[] + >(); + + private constructor(private readonly logger: LoggerService) {} + + static create(options: { logger: LoggerService }): DefaultEventsService { + return new DefaultEventsService(options.logger); + } + + /** + * Returns a plugin-scoped context of the `EventService` + * that ensures to prefix subscriber IDs with the plugin ID. + * + * @param pluginId - The plugin that the `EventService` should be created for. + */ + forPlugin(pluginId: string): EventsService { + return { + publish: (params: EventParams): Promise => { + return this.publish(params); + }, + subscribe: (options: EventsServiceSubscribeOptions): Promise => { + return this.subscribe({ + ...options, + id: `${pluginId}.${options.id}`, + }); + }, + }; + } + + async publish(params: EventParams): Promise { + this.logger.debug( + `Event received: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + + if (!this.subscribers.has(params.topic)) { + return; + } + + const onEventPromises: Promise[] = []; + this.subscribers.get(params.topic)?.forEach(subscription => { + onEventPromises.push( + (async () => { + try { + await subscription.onEvent(params); + } catch (error) { + this.logger.warn( + `Subscriber "${subscription.id}" failed to process event for topic "${params.topic}"`, + error, + ); + } + })(), + ); + }); + + await Promise.all(onEventPromises); + } + + async subscribe(options: EventsServiceSubscribeOptions): Promise { + options.topics.forEach(topic => { + if (!this.subscribers.has(topic)) { + this.subscribers.set(topic, []); + } + + this.subscribers.get(topic)!.push({ + id: options.id, + onEvent: options.onEvent, + }); + }); + } +} diff --git a/plugins/events-node/src/api/EventBroker.ts b/plugins/events-node/src/api/EventBroker.ts index 736c2a2bf0..f6afdf2f14 100644 --- a/plugins/events-node/src/api/EventBroker.ts +++ b/plugins/events-node/src/api/EventBroker.ts @@ -23,6 +23,7 @@ import { EventSubscriber } from './EventSubscriber'; * others can subscribe for future events for topics they are interested in. * * @public + * @deprecated use `EventsService` instead */ export interface EventBroker { /** diff --git a/plugins/events-node/src/api/EventPublisher.ts b/plugins/events-node/src/api/EventPublisher.ts index 285f427804..9089eb33fa 100644 --- a/plugins/events-node/src/api/EventPublisher.ts +++ b/plugins/events-node/src/api/EventPublisher.ts @@ -23,7 +23,11 @@ import { EventBroker } from './EventBroker'; * or from event brokers, queues, etc. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventPublisher { + /** + * @deprecated use the `EventsService` via the constructor, setter, or other means instead + */ setEventBroker(eventBroker: EventBroker): Promise; } diff --git a/plugins/events-node/src/api/EventSubscriber.ts b/plugins/events-node/src/api/EventSubscriber.ts index 439f49b890..3686a8db3f 100644 --- a/plugins/events-node/src/api/EventSubscriber.ts +++ b/plugins/events-node/src/api/EventSubscriber.ts @@ -22,10 +22,13 @@ import { EventParams } from './EventParams'; * or other actions to react on events. * * @public + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ export interface EventSubscriber { /** * Supported event topics like "github", "bitbucketCloud", etc. + * + * @deprecated use the `EventsService` via the constructor, setter, or other means instead */ supportsEventTopics(): string[]; @@ -33,6 +36,7 @@ export interface EventSubscriber { * React on a received event. * * @param params - parameters for the to be received event. + * @deprecated you are not required to expose this anymore when using `EventsService` */ onEvent(params: EventParams): Promise; } diff --git a/plugins/events-node/src/api/EventsService.ts b/plugins/events-node/src/api/EventsService.ts new file mode 100644 index 0000000000..7af13f9b07 --- /dev/null +++ b/plugins/events-node/src/api/EventsService.ts @@ -0,0 +1,57 @@ +/* + * 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 { EventParams } from './EventParams'; + +/** + * Allows a decoupled and asynchronous communication between components. + * Components can publish events for a given topic and + * others can subscribe for future events for topics they are interested in. + * + * @public + */ +export interface EventsService { + /** + * Publishes an event for the topic. + * + * @param params - parameters for the to be published event. + */ + publish(params: EventParams): Promise; + + /** + * Subscribes to one or more topics, registering an event handler for them. + * + * @param options - event subscription options. + */ + subscribe(options: EventsServiceSubscribeOptions): Promise; +} + +/** + * @public + */ +export type EventsServiceSubscribeOptions = { + /** + * Identifier for the subscription. E.g., used as part of log messages. + */ + id: string; + topics: string[]; + onEvent: EventsServiceEventHandler; +}; + +/** + * @public + */ +export type EventsServiceEventHandler = (params: EventParams) => Promise; diff --git a/plugins/events-node/src/api/index.ts b/plugins/events-node/src/api/index.ts index 91711c0e38..94d3014dff 100644 --- a/plugins/events-node/src/api/index.ts +++ b/plugins/events-node/src/api/index.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -export type { EventBroker } from './EventBroker'; export type { EventParams } from './EventParams'; -export type { EventPublisher } from './EventPublisher'; export { EventRouter } from './EventRouter'; -export type { EventSubscriber } from './EventSubscriber'; +export type { + EventsService, + EventsServiceSubscribeOptions, + EventsServiceEventHandler, +} from './EventsService'; +export { DefaultEventsService } from './DefaultEventsService'; export * from './http'; export { SubTopicEventRouter } from './SubTopicEventRouter'; diff --git a/plugins/events-node/src/deprecated.ts b/plugins/events-node/src/deprecated.ts new file mode 100644 index 0000000000..615e7b81ea --- /dev/null +++ b/plugins/events-node/src/deprecated.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export type { EventBroker } from './api/EventBroker'; +export type { EventPublisher } from './api/EventPublisher'; +export type { EventSubscriber } from './api/EventSubscriber'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 2e44b381af..90add52563 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -26,12 +26,21 @@ import { * @alpha */ export interface EventsExtensionPoint { + /** + * @deprecated use `eventsServiceRef` and `eventsServiceFactory` instead + */ setEventBroker(eventBroker: EventBroker): void; + /** + * @deprecated use `EventsService.publish` instead + */ addPublishers( ...publishers: Array> ): void; + /** + * @deprecated use `EventsService.subscribe` instead + */ addSubscribers( ...subscribers: Array> ): void; diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts index 2bdf456f13..2bd93a8aea 100644 --- a/plugins/events-node/src/index.ts +++ b/plugins/events-node/src/index.ts @@ -21,3 +21,5 @@ */ export * from './api'; +export * from './deprecated'; +export { eventsServiceRef } from './service'; diff --git a/plugins/events-node/src/service.ts b/plugins/events-node/src/service.ts new file mode 100644 index 0000000000..e1d3047fca --- /dev/null +++ b/plugins/events-node/src/service.ts @@ -0,0 +1,47 @@ +/* + * 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 { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { EventsService, DefaultEventsService } from './api'; + +/** + * The {@link EventsService} that allows to publish events, and subscribe to topics. + * Uses the `root` scope so that events can be shared across all plugins, modules, and more. + * + * @public + */ +export const eventsServiceRef = createServiceRef({ + id: 'events.service', + scope: 'plugin', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + pluginMetadata: coreServices.pluginMetadata, + rootLogger: coreServices.rootLogger, + }, + async createRootContext({ rootLogger }) { + return DefaultEventsService.create({ logger: rootLogger }); + }, + async factory({ pluginMetadata }, eventsService) { + return eventsService.forPlugin(pluginMetadata.getId()); + }, + }), +}); diff --git a/yarn.lock b/yarn.lock index 7271c5f6bb..f49f9f73c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6495,6 +6495,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-events-node@workspace:plugins/events-node" dependencies: + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown From eff3ca9ddd5da779e0fabf3e677f6a76e790a056 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 19:20:26 +0100 Subject: [PATCH 329/483] feat(events)!: migrate `EventRouter` implementations from `EventBroker` to `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/kind-students-cross.md | 76 +++++++++++++++++++ .../events-backend-module-azure/api-report.md | 5 +- .../events-backend-module-azure/package.json | 3 +- .../src/router/AzureDevOpsEventRouter.test.ts | 33 ++++---- .../src/router/AzureDevOpsEventRouter.ts | 12 ++- ...eventsModuleAzureDevOpsEventRouter.test.ts | 34 ++++----- .../eventsModuleAzureDevOpsEventRouter.ts | 12 +-- .../api-report.md | 5 +- .../package.json | 3 +- .../router/BitbucketCloudEventRouter.test.ts | 33 ++++---- .../src/router/BitbucketCloudEventRouter.ts | 12 ++- ...ntsModuleBitbucketCloudEventRouter.test.ts | 37 +++++---- .../eventsModuleBitbucketCloudEventRouter.ts | 12 +-- .../api-report.md | 5 +- .../events-backend-module-gerrit/package.json | 3 +- .../src/router/GerritEventRouter.test.ts | 33 ++++---- .../src/router/GerritEventRouter.ts | 12 ++- .../eventsModuleGerritEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGerritEventRouter.ts | 10 +-- .../api-report.md | 5 +- .../events-backend-module-github/package.json | 3 +- .../src/router/GithubEventRouter.test.ts | 33 ++++---- .../src/router/GithubEventRouter.ts | 12 ++- .../eventsModuleGithubEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGithubEventRouter.ts | 10 +-- .../api-report.md | 5 +- .../events-backend-module-gitlab/package.json | 3 +- .../src/router/GitlabEventRouter.test.ts | 33 ++++---- .../src/router/GitlabEventRouter.ts | 12 ++- .../eventsModuleGitlabEventRouter.test.ts | 34 ++++----- .../service/eventsModuleGitlabEventRouter.ts | 10 +-- plugins/events-node/api-report.md | 14 ++-- .../events-node/src/api/EventRouter.test.ts | 39 +++++----- plugins/events-node/src/api/EventRouter.ts | 44 ++++++++--- .../src/api/SubTopicEventRouter.test.ts | 35 ++++----- .../src/api/SubTopicEventRouter.ts | 12 +-- yarn.lock | 5 -- 37 files changed, 429 insertions(+), 288 deletions(-) create mode 100644 .changeset/kind-students-cross.md diff --git a/.changeset/kind-students-cross.md b/.changeset/kind-students-cross.md new file mode 100644 index 0000000000..9f9d2d40a6 --- /dev/null +++ b/.changeset/kind-students-cross.md @@ -0,0 +1,76 @@ +--- +'@backstage/plugin-events-backend-module-bitbucket-cloud': minor +'@backstage/plugin-events-backend-module-gerrit': minor +'@backstage/plugin-events-backend-module-github': minor +'@backstage/plugin-events-backend-module-gitlab': minor +'@backstage/plugin-events-backend-module-azure': minor +'@backstage/plugin-events-node': minor +--- + +BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + +`EventRouter` uses the new `EventsService` instead of the `EventBroker` now, +causing a breaking change to its signature. + +All of its extensions and implementations got adjusted accordingly. +(`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, +`GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + +Required adjustments were made to all backend modules for the new backend system, +now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + +**Migration:** + +Example for implementations of `SubTopicEventRouter`: + +```diff + import { + EventParams, ++ EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { +- constructor() { +- super('github'); ++ constructor(options: { events: EventsService }) { ++ super({ ++ events: options.events, ++ topic: 'github', ++ }); + } + ++ protected getSubscriberId(): string { ++ return 'GithubEventRouter'; ++ } ++ + // ... + } +``` + +Example for a direct extension of `EventRouter`: + +```diff + class MyEventRouter extends EventRouter { +- constructor(/* ... */) { ++ constructor(options: { ++ events: EventsService; ++ // ... ++ }) { +- super(); + // ... ++ super({ ++ events: options.events, ++ topics: topics, ++ }); + } ++ ++ protected getSubscriberId(): string { ++ return 'MyEventRouter'; ++ } +- +- supportsEventTopics(): string[] { +- return this.topics; +- } + } +``` diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md index 4460aeb510..66529ef2d4 100644 --- a/plugins/events-backend-module-azure/api-report.md +++ b/plugins/events-backend-module-azure/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 9187f74024..0ee9f2dbfc 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts index 56e761a8fc..837888a05a 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter'; describe('AzureDevOpsEventRouter', () => { - const eventRouter = new AzureDevOpsEventRouter(); + const events = new TestEventsService(); + const eventRouter = new AzureDevOpsEventRouter({ events: events }); const topic = 'azureDevOps'; const eventPayload = { eventType: 'test.type', test: 'payload' }; const metadata = {}; - it('no $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.eventType', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('azureDevOps.test.type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published).toHaveLength(1); + expect(events.published[0].topic).toEqual('azureDevOps.test.type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts index 11dd7546dd..de05abd032 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor() { - super('azureDevOps'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'azureDevOps', + }); + } + + protected getSubscriberId(): string { + return 'AzureDevOpsEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts index d67b5eb071..d2afb81851 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleAzureDevOpsEventRouter } from './eventsModuleAzureDevOpsEventRouter'; -import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; describe('eventsModuleAzureDevOpsEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: AzureDevOpsEventRouter | undefined; - let addedSubscriber: AzureDevOpsEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleAzureDevOpsEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleAzureDevOpsEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); }); }); diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts index 50bc384502..9741015477 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleAzureDevOpsEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new AzureDevOpsEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new AzureDevOpsEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md index 4795edd89a..ba4f61d739 100644 --- a/plugins/events-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 609fac199c..5273652f44 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts index b7a47984e4..65a4f1bf21 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter'; describe('BitbucketCloudEventRouter', () => { - const eventRouter = new BitbucketCloudEventRouter(); + const events = new TestEventsService(); + const eventRouter = new BitbucketCloudEventRouter({ events }); const topic = 'bitbucketCloud'; const eventPayload = { test: 'payload' }; const metadata = { 'x-event-key': 'test:type' }; - it('no x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('bitbucketCloud.test:type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('bitbucketCloud.test:type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts index 8350511d65..0f3ce09abf 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor() { - super('bitbucketCloud'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'bitbucketCloud', + }); + } + + protected getSubscriberId(): string { + return 'BitbucketCloudEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts index 025d994b4b..337e2206e4 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts @@ -14,32 +14,31 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleBitbucketCloudEventRouter } from './eventsModuleBitbucketCloudEventRouter'; -import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; describe('eventsModuleBitbucketCloudEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: BitbucketCloudEventRouter | undefined; - let addedSubscriber: BitbucketCloudEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleBitbucketCloudEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter); + await startTestBackend({ + features: [ + eventsServiceFactory(), + eventsModuleBitbucketCloudEventRouter(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts index 841a463001..648d6c67fb 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; /** @@ -31,13 +31,13 @@ export const eventsModuleBitbucketCloudEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new BitbucketCloudEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new BitbucketCloudEventRouter({ + events, + }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md index ba3c4dd29f..c75857aa43 100644 --- a/plugins/events-backend-module-gerrit/api-report.md +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -4,12 +4,15 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class GerritEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 889c60c685..436fbde891 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -42,8 +42,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts index 7302a26012..6c635fcbdf 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GerritEventRouter } from './GerritEventRouter'; describe('GerritEventRouter', () => { - const eventRouter = new GerritEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GerritEventRouter({ events: events }); const topic = 'gerrit'; const eventPayload = { type: 'test-type', test: 'payload' }; const metadata = {}; - it('no $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.type', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gerrit.test-type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gerrit.test-type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts index 3d97508b62..dac5aa34d5 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GerritEventRouter extends SubTopicEventRouter { - constructor() { - super('gerrit'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gerrit', + }); + } + + protected getSubscriberId(): string { + return 'GerritEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts index c11f4c42db..4fc971fcce 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleGerritEventRouter } from './eventsModuleGerritEventRouter'; -import { GerritEventRouter } from '../router/GerritEventRouter'; describe('eventsModuleGerritEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GerritEventRouter | undefined; - let addedSubscriber: GerritEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGerritEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GerritEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GerritEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGerritEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GerritEventRouter'); }); }); diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts index 780ff7f878..8d9792c4f4 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GerritEventRouter } from '../router/GerritEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGerritEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GerritEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GerritEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md index bcf9b8ed74..5341f86039 100644 --- a/plugins/events-backend-module-github/api-report.md +++ b/plugins/events-backend-module-github/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -15,8 +16,10 @@ export function createGithubSignatureValidator( // @public export class GithubEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index f78858d981..209167f64c 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -44,8 +44,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "@octokit/webhooks-methods": "^3.0.0", - "winston": "^3.2.1" + "@octokit/webhooks-methods": "^3.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts index 14cf6b9933..47f7eeb99f 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GithubEventRouter } from './GithubEventRouter'; describe('GithubEventRouter', () => { - const eventRouter = new GithubEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GithubEventRouter({ events: events }); const topic = 'github'; const eventPayload = { test: 'payload' }; const metadata = { 'x-github-event': 'test_type' }; - it('no x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-github-event', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('github.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('github.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts index 10dd1c55c6..767ed784f2 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GithubEventRouter extends SubTopicEventRouter { - constructor() { - super('github'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'github', + }); + } + + protected getSubscriberId(): string { + return 'GithubEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts index 02151d0dcf..f147bbcb69 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGithubEventRouter } from './eventsModuleGithubEventRouter'; -import { GithubEventRouter } from '../router/GithubEventRouter'; describe('eventsModuleGithubEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GithubEventRouter | undefined; - let addedSubscriber: GithubEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGithubEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GithubEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GithubEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGithubEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GithubEventRouter'); }); }); diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts index 093307dfaf..694b4b162d 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GithubEventRouter } from '../router/GithubEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGithubEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GithubEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GithubEventRouter({ events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md index 8a0c513857..f348436375 100644 --- a/plugins/events-backend-module-gitlab/api-report.md +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,8 +14,10 @@ export function createGitlabTokenValidator(config: Config): RequestValidator; // @public export class GitlabEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { events: EventsService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + protected getSubscriberId(): string; } ``` diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index f59b6e5faf..08abb12b8f 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -43,8 +43,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-events-node": "workspace:^", - "winston": "^3.2.1" + "@backstage/plugin-events-node": "workspace:^" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts index 6ced12d3cb..bc9da24cbe 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { GitlabEventRouter } from './GitlabEventRouter'; describe('GitlabEventRouter', () => { - const eventRouter = new GitlabEventRouter(); + const events = new TestEventsService(); + const eventRouter = new GitlabEventRouter({ events: events }); const topic = 'gitlab'; const eventPayload = { event_name: 'test_type', test: 'payload' }; const metadata = {}; - it('no $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + events.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); + expect(events.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.event_name', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(events.published).toEqual([]); }); it('with $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gitlab.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(events.published.length).toBe(1); + expect(events.published[0].topic).toEqual('gitlab.test_type'); + expect(events.published[0].eventPayload).toEqual(eventPayload); + expect(events.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts index 16324340ee..23b0389b55 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventsService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,15 @@ import { * @public */ export class GitlabEventRouter extends SubTopicEventRouter { - constructor() { - super('gitlab'); + constructor(options: { events: EventsService }) { + super({ + events: options.events, + topic: 'gitlab', + }); + } + + protected getSubscriberId(): string { + return 'GitlabEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts index 34a68ccbe4..9195be7a73 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGitlabEventRouter } from './eventsModuleGitlabEventRouter'; -import { GitlabEventRouter } from '../router/GitlabEventRouter'; describe('eventsModuleGitlabEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GitlabEventRouter | undefined; - let addedSubscriber: GitlabEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; - - await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGitlabEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GitlabEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter); + await startTestBackend({ + features: [eventsServiceFactory(), eventsModuleGitlabEventRouter()], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('GitlabEventRouter'); }); }); diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts index fc44e95057..66245efb58 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGitlabEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + events: eventsServiceRef, }, async init({ events }) { - const eventRouter = new GitlabEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + const eventRouter = new GitlabEventRouter({ events: events }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 081c56549a..9574d6c098 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -39,17 +39,17 @@ export interface EventPublisher { } // @public -export abstract class EventRouter implements EventPublisher, EventSubscriber { +export abstract class EventRouter { + protected constructor(options: { events: EventsService; topics: string[] }); // (undocumented) protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; // (undocumented) + protected abstract getSubscriberId(): string; + // (undocumented) onEvent(params: EventParams): Promise; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; - // (undocumented) - abstract supportsEventTopics(): string[]; + subscribe(): Promise; } // @public @@ -114,12 +114,10 @@ export type RequestValidator = ( // @public export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(topic: string); + protected constructor(options: { events: EventsService; topic: string }); // (undocumented) protected determineDestinationTopic(params: EventParams): string | undefined; // (undocumented) protected abstract determineSubTopic(params: EventParams): string | undefined; - // (undocumented) - supportsEventTopics(): string[]; } ``` diff --git a/plugins/events-node/src/api/EventRouter.test.ts b/plugins/events-node/src/api/EventRouter.test.ts index 551c5ea67d..f7709ead64 100644 --- a/plugins/events-node/src/api/EventRouter.test.ts +++ b/plugins/events-node/src/api/EventRouter.test.ts @@ -14,11 +14,19 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; class TestEventRouter extends EventRouter { + constructor(events: EventsService) { + super({ events, topics: ['my-topic'] }); + } + + protected getSubscriberId(): string { + return 'TestEventRouter'; + } + protected determineDestinationTopic(params: EventParams): string | undefined { const payload = params.eventPayload as { value?: number }; if (payload.value === undefined) { @@ -27,26 +35,21 @@ class TestEventRouter extends EventRouter { return payload.value % 2 === 0 ? 'even' : 'odd'; } - - supportsEventTopics(): string[] { - return ['my-topic']; - } } describe('EventRouter', () => { - const eventRouter = new TestEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestEventRouter(events); const topic = 'my-topic'; const metadata = { random: 'metadata' }; it('no destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload: { discarded: 'event' }, @@ -57,14 +60,6 @@ describe('EventRouter', () => { }); it('with destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - const payloadEven = { value: 2 }; const payloadOdd = { value: 3 }; await eventRouter.onEvent({ topic, eventPayload: payloadEven, metadata }); diff --git a/plugins/events-node/src/api/EventRouter.ts b/plugins/events-node/src/api/EventRouter.ts index b435ef15f4..5e4492d25f 100644 --- a/plugins/events-node/src/api/EventRouter.ts +++ b/plugins/events-node/src/api/EventRouter.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; -import { EventPublisher } from './EventPublisher'; -import { EventSubscriber } from './EventSubscriber'; +import { EventsService } from './EventsService'; /** * Subscribes to a topic and - depending on a set of conditions - @@ -26,13 +24,41 @@ import { EventSubscriber } from './EventSubscriber'; * @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}. * @public */ -export abstract class EventRouter implements EventPublisher, EventSubscriber { - private eventBroker?: EventBroker; +export abstract class EventRouter { + private readonly events: EventsService; + private readonly topics: string[]; + private subscribed: boolean = false; + + protected constructor(options: { events: EventsService; topics: string[] }) { + this.events = options.events; + this.topics = options.topics; + } + + protected abstract getSubscriberId(): string; protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; + /** + * Subscribes itself to the topic(s), + * after which events potentially can be received + * and processed by {@link EventRouter.onEvent}. + */ + async subscribe(): Promise { + if (this.subscribed) { + return; + } + + this.subscribed = true; + + await this.events.subscribe({ + id: this.getSubscriberId(), + topics: this.topics, + onEvent: this.onEvent.bind(this), + }); + } + async onEvent(params: EventParams): Promise { const topic = this.determineDestinationTopic(params); @@ -41,15 +67,9 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // republish to different topic - this.eventBroker?.publish({ + await this.events.publish({ ...params, topic, }); } - - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - - abstract supportsEventTopics(): string[]; } diff --git a/plugins/events-node/src/api/SubTopicEventRouter.test.ts b/plugins/events-node/src/api/SubTopicEventRouter.test.ts index d5c79895a5..6298e1549d 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.test.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.test.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; +import { EventsService } from './EventsService'; import { SubTopicEventRouter } from './SubTopicEventRouter'; class TestSubTopicEventRouter extends SubTopicEventRouter { - constructor() { - super('my-topic'); + constructor(events: EventsService) { + super({ events, topic: 'my-topic' }); + } + + protected getSubscriberId(): string { + return 'TestSubTopicEventRouter'; } protected determineSubTopic(params: EventParams): string | undefined { @@ -29,34 +33,25 @@ class TestSubTopicEventRouter extends SubTopicEventRouter { } describe('SubTopicEventRouter', () => { - const eventRouter = new TestSubTopicEventRouter(); + const published: EventParams[] = []; + const events: EventsService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestSubTopicEventRouter(events); const topic = 'my-topic'; const eventPayload = { test: 'payload' }; const metadata = { 'x-my-event': 'test.type' }; it('no x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload }); expect(published).toEqual([]); }); it('with x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload, metadata }); expect(published.length).toBe(1); diff --git a/plugins/events-node/src/api/SubTopicEventRouter.ts b/plugins/events-node/src/api/SubTopicEventRouter.ts index 04abe14009..5a96ad6788 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventsService } from './EventsService'; /** * Subscribes to the provided (generic) topic @@ -27,8 +28,11 @@ import { EventRouter } from './EventRouter'; * @public */ export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(private readonly topic: string) { - super(); + protected constructor(options: { events: EventsService; topic: string }) { + super({ + events: options.events, + topics: [options.topic], + }); } protected abstract determineSubTopic(params: EventParams): string | undefined; @@ -37,8 +41,4 @@ export abstract class SubTopicEventRouter extends EventRouter { const subTopic = this.determineSubTopic(params); return subTopic ? `${params.topic}.${subTopic}` : undefined; } - - supportsEventTopics(): string[] { - return [this.topic]; - } } diff --git a/yarn.lock b/yarn.lock index f49f9f73c5..39bfa662d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6404,7 +6404,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6417,7 +6416,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6430,7 +6428,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6445,7 +6442,6 @@ __metadata: "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@octokit/webhooks-methods": ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft @@ -6459,7 +6455,6 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - winston: ^3.2.1 languageName: unknown linkType: soft From c4bd79422ab24f92bf706a79ccc1fb11446ff932 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 20:28:35 +0100 Subject: [PATCH 330/483] feat(events)!: migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/young-flies-wash.md | 35 +++++++++++ packages/backend/src/index.ts | 8 ++- packages/backend/src/plugins/events.ts | 11 +--- packages/backend/src/types.ts | 6 +- plugins/events-backend/api-report.md | 7 +-- .../src/service/EventsPlugin.test.ts | 56 +++++++++++------- .../src/service/EventsPlugin.ts | 59 ++++++------------- .../HttpPostIngressEventPublisher.test.ts | 40 ++++++------- .../http/HttpPostIngressEventPublisher.ts | 23 +++----- 9 files changed, 132 insertions(+), 113 deletions(-) create mode 100644 .changeset/young-flies-wash.md diff --git a/.changeset/young-flies-wash.md b/.changeset/young-flies-wash.md new file mode 100644 index 0000000000..dcedc9bb7c --- /dev/null +++ b/.changeset/young-flies-wash.md @@ -0,0 +1,35 @@ +--- +'@backstage/plugin-events-backend': minor +--- + +BREAKING CHANGE: Migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `HttpPostIngressEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `HttpPostIngressEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + }); + http.bind(eventsRouter); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(http) +- .start(); + + // or for other kinds of setups +- await Promise.all(http.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsPlugin` uses the `eventsServiceRef` as dependency. +Unsupported (and deprecated) extension point methods will throw an error to prevent unintended behavior. + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +``` diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bade028597..8931e6832b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -70,6 +70,7 @@ import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { DefaultEventBroker } from '@backstage/plugin-events-backend'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import { metrics } from '@opentelemetry/api'; @@ -99,7 +100,11 @@ function makeCreateEnv(config: Config) { discovery, }); - const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); + const eventsService = DefaultEventsService.create({ logger: root }); + const eventBroker = new DefaultEventBroker( + root.child({ type: 'plugin' }), + eventsService, + ); const signalService = DefaultSignalService.create({ eventBroker, }); @@ -119,6 +124,7 @@ function makeCreateEnv(config: Config) { config, reader, eventBroker, + events: eventsService, discovery, tokenManager, permissions, diff --git a/packages/backend/src/plugins/events.ts b/packages/backend/src/plugins/events.ts index f3ff354240..fd60a9bb14 100644 --- a/packages/backend/src/plugins/events.ts +++ b/packages/backend/src/plugins/events.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - EventsBackend, - HttpPostIngressEventPublisher, -} from '@backstage/plugin-events-backend'; +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -28,14 +25,10 @@ export default async function createPlugin( const http = HttpPostIngressEventPublisher.fromConfig({ config: env.config, + events: env.events, logger: env.logger, }); http.bind(eventsRouter); - await new EventsBackend(env.logger) - .setEventBroker(env.eventBroker) - .addPublishers(http) - .start(); - return eventsRouter; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index d76e68c1c9..7d9cd19310 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,7 +26,7 @@ import { import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventBroker, EventsService } from '@backstage/plugin-events-node'; import { SignalService } from '@backstage/plugin-signals-node'; export type PluginEnvironment = { @@ -40,6 +40,10 @@ export type PluginEnvironment = { permissions: PermissionEvaluator; scheduler: PluginTaskScheduler; identity: IdentityApi; + /** + * @deprecated use `events` instead + */ eventBroker: EventBroker; + events: EventsService; signalService: SignalService; }; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index 9fffe719ba..8b5d4e8d31 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -43,18 +43,17 @@ export class EventsBackend { } // @public -export class HttpPostIngressEventPublisher implements EventPublisher { +export class HttpPostIngressEventPublisher { // (undocumented) bind(router: express.Router): void; // (undocumented) static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit; }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; } ``` diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 7f555a0b6f..e39951e8dc 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -14,22 +14,27 @@ * limitations under the License. */ -import { createBackendModule } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { - TestEventBroker, - TestEventPublisher, - TestEventSubscriber, -} from '@backstage/plugin-events-backend-test-utils'; + createBackendModule, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import request from 'supertest'; import { eventsPlugin } from './EventsPlugin'; -describe('eventPlugin', () => { +describe('eventsPlugin', () => { it('should be initialized properly', async () => { - const eventBroker = new TestEventBroker(); - const publisher = new TestEventPublisher(); - const subscriber = new TestEventSubscriber('sub', ['fake']); + const eventsService = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return eventsService; + }, + }); const testModule = createBackendModule({ pluginId: 'events', @@ -40,9 +45,9 @@ describe('eventPlugin', () => { events: eventsExtensionPoint, }, async init({ events }) { - events.setEventBroker(eventBroker); - events.addPublishers(publisher); - events.addSubscribers(subscriber); + events.addHttpPostIngress({ + topic: 'fake-ext', + }); }, }); }, @@ -51,6 +56,7 @@ describe('eventPlugin', () => { const { server } = await startTestBackend({ extensionPoints: [], features: [ + eventsServiceFactory(), eventsPlugin(), testModule(), mockServices.logger.factory(), @@ -66,18 +72,24 @@ describe('eventPlugin', () => { ], }); - expect(publisher.eventBroker).toBe(eventBroker); - expect(eventBroker.subscribed.length).toEqual(1); - expect(eventBroker.subscribed[0]).toBe(subscriber); - - const response = await request(server) + const response1 = await request(server) .post('/api/events/http/fake') .timeout(1000) .send({ test: 'fake' }); - expect(response.status).toBe(202); + expect(response1.status).toBe(202); - expect(eventBroker.published.length).toEqual(1); - expect(eventBroker.published[0].topic).toEqual('fake'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' }); + const response2 = await request(server) + .post('/api/events/http/fake-ext') + .timeout(1000) + .send({ test: 'fake-ext' }); + expect(response2.status).toBe(202); + + expect(eventsService.published).toHaveLength(2); + expect(eventsService.published[0].topic).toEqual('fake'); + expect(eventsService.published[0].eventPayload).toEqual({ test: 'fake' }); + expect(eventsService.published[1].topic).toEqual('fake-ext'); + expect(eventsService.published[1].eventPayload).toEqual({ + test: 'fake-ext', + }); }); }); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 27456b5b2a..5e1df975c5 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -18,59 +18,42 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { eventsExtensionPoint, EventsExtensionPoint, } from '@backstage/plugin-events-node/alpha'; import { - EventBroker, - EventPublisher, - EventSubscriber, + eventsServiceRef, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { DefaultEventBroker } from './DefaultEventBroker'; import Router from 'express-promise-router'; import { HttpPostIngressEventPublisher } from './http'; class EventsExtensionPointImpl implements EventsExtensionPoint { - #eventBroker: EventBroker | undefined; #httpPostIngresses: HttpPostIngressOptions[] = []; - #publishers: EventPublisher[] = []; - #subscribers: EventSubscriber[] = []; - setEventBroker(eventBroker: EventBroker): void { - this.#eventBroker = eventBroker; + setEventBroker(_: any): void { + throw new Error( + 'setEventBroker is not supported anymore; use eventsServiceRef instead', + ); } - addPublishers( - ...publishers: Array> - ): void { - this.#publishers.push(...publishers.flat()); + addPublishers(_: any): void { + throw new Error( + 'addPublishers is not supported anymore; use EventsService instead', + ); } - addSubscribers( - ...subscribers: Array> - ): void { - this.#subscribers.push(...subscribers.flat()); + addSubscribers(_: any): void { + throw new Error( + 'addSubscribers is not supported anymore; use EventsService instead', + ); } addHttpPostIngress(options: HttpPostIngressOptions) { this.#httpPostIngresses.push(options); } - get eventBroker() { - return this.#eventBroker; - } - - get publishers() { - return this.#publishers; - } - - get subscribers() { - return this.#subscribers; - } - get httpPostIngresses() { return this.#httpPostIngresses; } @@ -90,12 +73,11 @@ export const eventsPlugin = createBackendPlugin({ env.registerInit({ deps: { config: coreServices.rootConfig, + events: eventsServiceRef, logger: coreServices.logger, router: coreServices.httpRouter, }, - async init({ config, logger, router }) { - const winstonLogger = loggerToWinstonLogger(logger); - + async init({ config, events, logger, router }) { const ingresses = Object.fromEntries( extensionPoint.httpPostIngresses.map(ingress => [ ingress.topic, @@ -105,20 +87,13 @@ export const eventsPlugin = createBackendPlugin({ const http = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses, - logger: winstonLogger, + logger, }); const eventsRouter = Router(); http.bind(eventsRouter); router.use(eventsRouter); - - const eventBroker = - extensionPoint.eventBroker ?? new DefaultEventBroker(winstonLogger); - - eventBroker.subscribe(extensionPoint.subscribers); - [extensionPoint.publishers, http] - .flat() - .forEach(publisher => publisher.setEventBroker(eventBroker)); }, }); }, diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 72e0b94c57..665ac0b4a9 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -16,7 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import express from 'express'; import Router from 'express-promise-router'; import request from 'supertest'; @@ -36,9 +36,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: {}, }, @@ -46,9 +48,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const notFoundResponse = await request(app) .post('/http/unknown') .timeout(1000) @@ -69,18 +68,18 @@ describe('HttpPostIngressEventPublisher', () => { .send({ testB: 'data' }); expect(response2.status).toBe(202); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' }); - expect(eventBroker.published[0].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ testA: 'data' }); + expect(events.published[0].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }), ); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ testB: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', @@ -99,9 +98,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const events = new TestEventsService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + events, ingresses: { testB: { validator: async (req, context) => { @@ -146,9 +147,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const response1 = await request(app) .post('/http/testA') .timeout(1000) @@ -191,12 +189,12 @@ describe('HttpPostIngressEventPublisher', () => { expect(response6.status).toBe(403); expect(response6.body).toEqual({}); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('testA'); + expect(events.published[0].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].topic).toEqual('testB'); + expect(events.published[1].eventPayload).toEqual({ test: 'data' }); + expect(events.published[1].metadata).toEqual( expect.objectContaining({ 'x-test-signature': 'testB-signature', }), @@ -205,10 +203,12 @@ describe('HttpPostIngressEventPublisher', () => { it('without configuration', async () => { const config = new ConfigReader({}); + const events = new TestEventsService(); expect(() => HttpPostIngressEventPublisher.fromConfig({ config, + events, logger, }), ).not.toThrow(); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index b5a6ccbca1..06dc4e463a 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -15,16 +15,15 @@ */ import { errorHandler } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { - EventBroker, - EventPublisher, + EventsService, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { RequestValidationContextImpl } from './validation'; /** @@ -34,13 +33,12 @@ import { RequestValidationContextImpl } from './validation'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class HttpPostIngressEventPublisher implements EventPublisher { - private eventBroker?: EventBroker; - +export class HttpPostIngressEventPublisher { static fromConfig(env: { config: Config; + events: EventsService; ingresses?: { [topic: string]: Omit }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher { const topics = env.config.getOptionalStringArray('events.http.topics') ?? []; @@ -54,11 +52,12 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } }); - return new HttpPostIngressEventPublisher(env.logger, ingresses); + return new HttpPostIngressEventPublisher(env.events, env.logger, ingresses); } private constructor( - private readonly logger: Logger, + private readonly events: EventsService, + private readonly logger: LoggerService, private readonly ingresses: { [topic: string]: Omit; }, @@ -68,10 +67,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher { router.use('/http', this.createRouter(this.ingresses)); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - private createRouter(ingresses: { [topic: string]: Omit; }): express.Router { @@ -108,7 +103,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } const eventPayload = request.body; - await this.eventBroker!.publish({ + await this.events.publish({ topic, eventPayload, metadata: request.headers, From 8f6afa94a9dc80ae11d5c1acd3ad70e67c9134f1 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 20:40:22 +0100 Subject: [PATCH 331/483] chore(events): migrate `DemoEventBasedEntityProvider` to use `EventsService` Signed-off-by: Patrick Jungermann --- .../plugins/DemoEventBasedEntityProvider.ts | 42 +++++++++---------- packages/backend/src/plugins/catalog.ts | 3 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts index 7a032198a3..11d073400a 100644 --- a/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts +++ b/packages/backend/src/plugins/DemoEventBasedEntityProvider.ts @@ -18,40 +18,36 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; -import { - EventBroker, - EventParams, - EventSubscriber, -} from '@backstage/plugin-events-node'; +import { EventParams, EventsService } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; -export class DemoEventBasedEntityProvider - implements EntityProvider, EventSubscriber -{ +export class DemoEventBasedEntityProvider implements EntityProvider { private readonly logger: Logger; + private readonly events: EventsService; private readonly topics: string[]; constructor(opts: { - eventBroker: EventBroker; + events: EventsService; logger: Logger; topics: string[]; }) { - const { eventBroker, logger, topics } = opts; - this.logger = logger; - this.topics = topics; - eventBroker.subscribe(this); + this.events = opts.events; + this.logger = opts.logger; + this.topics = opts.topics; } - async onEvent(params: EventParams): Promise { - this.logger.info( - `onEvent: topic=${params.topic}, metadata=${JSON.stringify( - params.metadata, - )}, payload=${JSON.stringify(params.eventPayload)}`, - ); - } - - supportsEventTopics(): string[] { - return this.topics; + async subscribe() { + await this.events.subscribe({ + id: 'DemoEventBasedEntityProvider', + topics: this.topics, + onEvent: async (params: EventParams): Promise => { + this.logger.info( + `onEvent: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + }, + }); } async connect(_: EntityProviderConnection): Promise { diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 223acab818..00fe7ff4a0 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -28,10 +28,11 @@ export default async function createPlugin( builder.addProcessor(new ScaffolderEntitiesProcessor()); const demoProvider = new DemoEventBasedEntityProvider({ + events: env.events, logger: env.logger, topics: ['example'], - eventBroker: env.eventBroker, }); + await demoProvider.subscribe(); builder.addEntityProvider(demoProvider); const { processingEngine, router } = await builder.build(); From 132d672747d688d81c5eab76b2241712bf697b30 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 21:01:42 +0100 Subject: [PATCH 332/483] feat(events)!: migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService` Signed-off-by: Patrick Jungermann --- .changeset/long-emus-talk.md | 32 +++++++++++++++++++ .../api-report.md | 12 +++---- .../package.json | 3 +- .../AwsSqsConsumingEventPublisher.test.ts | 30 +++++++++-------- .../AwsSqsConsumingEventPublisher.ts | 29 +++++++++-------- ...oduleAwsSqsConsumingEventPublisher.test.ts | 28 +++++++--------- ...entsModuleAwsSqsConsumingEventPublisher.ts | 15 ++++----- yarn.lock | 1 - 8 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 .changeset/long-emus-talk.md diff --git a/.changeset/long-emus-talk.md b/.changeset/long-emus-talk.md new file mode 100644 index 0000000000..ac8d5cbc68 --- /dev/null +++ b/.changeset/long-emus-talk.md @@ -0,0 +1,32 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': minor +--- + +BREAKING CHANGE: Migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService`. + +Uses the `EventsService` instead of `EventBroker` at `AwsSqsConsumingEventPublisher`, +dropping the use of `EventPublisher` including `setEventBroker(..)`. + +Now, `AwsSqsConsumingEventPublisher.fromConfig` requires `events: EventsService` as option. + +```diff + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); ++ await Promise.all(sqs.map(publisher => publisher.start())); + + // e.g. at packages/backend/src/plugins/events.ts +- await new EventsBackend(env.logger) +- .setEventBroker(env.eventBroker) +- .addPublishers(sqs) +- .start(); + + // or for other kinds of setups +- await Promise.all(sqs.map(publisher => publisher.setEventBroker(eventBroker))); +``` + +`eventsModuleAwsSqsConsumingEventPublisher` uses the `eventsServiceRef` as dependency, +instead of `eventsExtensionPoint`. diff --git a/plugins/events-backend-module-aws-sqs/api-report.md b/plugins/events-backend-module-aws-sqs/api-report.md index ffa9c888d6..ff863e4d8d 100644 --- a/plugins/events-backend-module-aws-sqs/api-report.md +++ b/plugins/events-backend-module-aws-sqs/api-report.md @@ -4,20 +4,20 @@ ```ts import { Config } from '@backstage/config'; -import { EventBroker } from '@backstage/plugin-events-node'; -import { EventPublisher } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; // @public -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { // (undocumented) static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[]; // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; + start(): Promise; } ``` diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 0b70eb57bf..41f91d804e 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -48,8 +48,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", - "luxon": "^3.0.0", - "winston": "^3.2.1" + "luxon": "^3.0.0" }, "devDependencies": { "@aws-sdk/types": "^3.347.0", diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts index e32245b483..60930fdcf7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts @@ -22,7 +22,7 @@ import { import { getVoidLogger } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { mockClient } from 'aws-sdk-client-mock'; import { AwsSqsConsumingEventPublisher } from './AwsSqsConsumingEventPublisher'; @@ -53,12 +53,14 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); @@ -85,21 +87,21 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); const scheduler = { scheduleTask: jest.fn(), } as unknown as PluginTaskScheduler; const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( @@ -133,6 +135,7 @@ describe('AwsSqsConsumingEventPublisher', () => { }, }); const logger = getVoidLogger(); + const events = new TestEventsService(); let taskFn: (() => Promise) | undefined = undefined; const scheduler = { scheduleTask: (spec: { fn: () => Promise }) => { @@ -196,32 +199,31 @@ describe('AwsSqsConsumingEventPublisher', () => { const publishers = AwsSqsConsumingEventPublisher.fromConfig({ config, + events, logger, scheduler, }); expect(publishers.length).toEqual(1); const publisher = publishers[0]; - - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); + await publisher.start(); await taskFn!(); await taskFn!(); await taskFn!(); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('fake1'); - expect(eventBroker.published[0].eventPayload).toEqual({ + expect(events.published).toHaveLength(2); + expect(events.published[0].topic).toEqual('fake1'); + expect(events.published[0].eventPayload).toEqual({ event: 'payload1', }); - expect(eventBroker.published[0].metadata).toEqual({ + expect(events.published[0].metadata).toEqual({ 'X-Custom-Attr': 'value', }); - expect(eventBroker.published[1].topic).toEqual('fake1'); - expect(eventBroker.published[1].eventPayload).toEqual({ + expect(events.published[1].topic).toEqual('fake1'); + expect(events.published[1].eventPayload).toEqual({ event: 'payload2', }); - expect(eventBroker.published[1].metadata).toEqual({}); + expect(events.published[1].metadata).toEqual({}); }); }); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index a2f6a00fe7..26eba88c18 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -21,10 +21,10 @@ import { ReceiveMessageCommandInput, SQSClient, } from '@aws-sdk/client-sqs'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; import { AwsSqsEventSourceConfig, readConfig } from './config'; /** @@ -34,28 +34,34 @@ import { AwsSqsEventSourceConfig, readConfig } from './config'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class AwsSqsConsumingEventPublisher implements EventPublisher { +export class AwsSqsConsumingEventPublisher { private readonly topic: string; private readonly receiveParams: ReceiveMessageCommandInput; private readonly sqs: SQSClient; private readonly queueUrl: string; private readonly taskTimeoutSeconds: number; private readonly waitTimeAfterEmptyReceiveMs; - private eventBroker?: EventBroker; static fromConfig(env: { config: Config; - logger: Logger; + events: EventsService; + logger: LoggerService; scheduler: PluginTaskScheduler; }): AwsSqsConsumingEventPublisher[] { return readConfig(env.config).map( config => - new AwsSqsConsumingEventPublisher(env.logger, env.scheduler, config), + new AwsSqsConsumingEventPublisher( + env.logger, + env.events, + env.scheduler, + config, + ), ); } private constructor( - private readonly logger: Logger, + private readonly logger: LoggerService, + private readonly events: EventsService, private readonly scheduler: PluginTaskScheduler, config: AwsSqsEventSourceConfig, ) { @@ -80,12 +86,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { config.waitTimeAfterEmptyReceive.as('milliseconds'); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - return this.start(); - } - - private async start(): Promise { + async start(): Promise { const id = `events.awsSqs.publisher:${this.topic}`; const logger = this.logger.child({ class: AwsSqsConsumingEventPublisher.prototype.constructor.name, @@ -172,7 +173,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { } }); - this.eventBroker!.publish({ + this.events.publish({ topic: this.topic, eventPayload, metadata, diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts index 57e1831667..cbf29f96cd 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts @@ -14,26 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleAwsSqsConsumingEventPublisher } from './eventsModuleAwsSqsConsumingEventPublisher'; -import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; describe('eventsModuleAwsSqsConsumingEventPublisher', () => { it('should be correctly wired and set up', async () => { - let addedPublishers: AwsSqsConsumingEventPublisher[] | undefined; - const extensionPoint = { - addPublishers: (publishers: any) => { - addedPublishers = publishers; + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; }, - }; + }); const scheduler = mockServices.scheduler.mock(); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], features: [ + eventsServiceFactory(), eventsModuleAwsSqsConsumingEventPublisher(), mockServices.rootConfig.factory({ data: { @@ -65,14 +67,6 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { ], }); - expect(addedPublishers).not.toBeUndefined(); - expect(addedPublishers!.length).toEqual(2); - - const eventBroker = new TestEventBroker(); - await Promise.all( - addedPublishers!.map(publisher => publisher.setEventBroker(eventBroker)), - ); - // publisher.connect(..) was causing the polling for events to be scheduled expect(scheduler.scheduleTask).toHaveBeenCalledWith( expect.objectContaining({ id: 'events.awsSqs.publisher:fake1' }), diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts index eabab94708..ea0f094bee 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.ts @@ -18,8 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; /** @@ -34,19 +33,19 @@ export const eventsModuleAwsSqsConsumingEventPublisher = createBackendModule({ env.registerInit({ deps: { config: coreServices.rootConfig, - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, }, async init({ config, events, logger, scheduler }) { - const winstonLogger = loggerToWinstonLogger(logger); const sqs = AwsSqsConsumingEventPublisher.fromConfig({ - config: config, - logger: winstonLogger, - scheduler: scheduler, + config, + events, + logger, + scheduler, }); - events.addPublishers(sqs); + await Promise.all(sqs.map(publisher => publisher.start())); }, }); }, diff --git a/yarn.lock b/yarn.lock index 39bfa662d7..a876413ec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6391,7 +6391,6 @@ __metadata: "@backstage/types": "workspace:^" aws-sdk-client-mock: ^3.0.0 luxon: ^3.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft From 52479092dc8efcd3057218bb2398ab5f81d0b09c Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 21:09:52 +0100 Subject: [PATCH 333/483] feat(events): add `events: EventsService` to `LegacyPluginEnvironment` Signed-off-by: Patrick Jungermann --- .changeset/six-nails-hammer.md | 5 +++++ packages/backend-dynamic-feature-service/api-report.md | 2 ++ .../backend-dynamic-feature-service/src/manager/types.ts | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/six-nails-hammer.md diff --git a/.changeset/six-nails-hammer.md b/.changeset/six-nails-hammer.md new file mode 100644 index 0000000000..d0cf7a0ea7 --- /dev/null +++ b/.changeset/six-nails-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +--- + +Add `events: EventsService` to `LegacyPluginEnvironment`. diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 64bffb468c..8075acabfb 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -10,6 +10,7 @@ import { Config } from '@backstage/config'; import { ConfigSchema } from '@backstage/config-loader'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; +import { EventsService } from '@backstage/plugin-events-node'; import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -215,6 +216,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index 0038470686..60b2eb831e 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -29,6 +29,7 @@ import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { EventBroker, + EventsService, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; @@ -64,6 +65,7 @@ export type LegacyPluginEnvironment = { scheduler: PluginTaskScheduler; identity: IdentityApi; eventBroker: EventBroker; + events: EventsService; pluginProvider: BackendPluginProvider; }; From 3c5f58622e866ba63f6de855476dacd1a831a36d Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 22:36:52 +0100 Subject: [PATCH 334/483] docs(events): update docs about events-backend used with the new backend system Signed-off-by: Patrick Jungermann --- .../building-backends/08-migrating.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 9eada30ef6..27e4eb7324 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -632,7 +632,7 @@ A basic installation of the events plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); ``` If you have other customizations made to `plugins/events.ts`, such as adding @@ -646,6 +646,7 @@ depends on the appropriate extension point and interacts with it. ```ts title="packages/backend/src/index.ts" /* highlight-add-start */ +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { createBackendModule } from '@backstage/backend-plugin-api'; /* highlight-add-end */ @@ -663,7 +664,28 @@ const eventsModuleCustomExtensions = createBackendModule({ async init({ events /* ..., other dependencies */ }) { // Here you have the opportunity to interact with the extension // point before the plugin itself gets instantiated - events.addSubscribers(new MySubscriber()); // just an example + events.addHttpPostIngress({ + // ... + }); + }, + }); + }, +}); +/* highlight-add-end */ + +/* highlight-add-start */ +const otherPluginModuleCustomExtensions = createBackendModule({ + pluginId: 'other-plugin', // name of the plugin that the module is targeting + moduleId: 'custom-extensions', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + // ... and other dependencies as needed + }, + async init({ events /* ..., other dependencies */ }) { + // Here you have the opportunity to interact with the extension + // point before the plugin itself gets instantiated }, }); }, @@ -671,17 +693,11 @@ const eventsModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-events-backend')); +backend.add(import('@backstage/plugin-events-backend/alpha')); /* highlight-add-next-line */ backend.add(eventsModuleCustomExtensions()); -``` - -This also requires that you have a dependency on the corresponding node package, -if you didn't already have one. - -```bash -# from the repository root -yarn --cwd packages/backend add @backstage/plugin-events-node +/* highlight-add-next-line */ +backend.add(otherPluginModuleCustomExtensions()); ``` Here we've placed the module directly in the backend index file just to get From 1ab76e523de915c497277ed8d7ebdf03aad09579 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Wed, 24 Jan 2024 03:06:54 +0100 Subject: [PATCH 335/483] docs(events): describe the new events setup, update README.md files Signed-off-by: Patrick Jungermann --- .../events-backend-module-aws-sqs/README.md | 48 ++- plugins/events-backend-module-azure/README.md | 32 +- .../README.md | 32 +- .../events-backend-module-gerrit/README.md | 28 +- .../events-backend-module-github/README.md | 52 ++-- .../events-backend-module-gitlab/README.md | 52 ++-- plugins/events-backend-test-utils/README.md | 7 +- plugins/events-backend/README.md | 277 +++++------------- plugins/events-node/README.md | 83 +++++- 9 files changed, 295 insertions(+), 316 deletions(-) diff --git a/plugins/events-backend-module-aws-sqs/README.md b/plugins/events-backend-module-aws-sqs/README.md index 7ca4bb280d..bd8e0c51d3 100644 --- a/plugins/events-backend-module-aws-sqs/README.md +++ b/plugins/events-backend-module-aws-sqs/README.md @@ -1,12 +1,12 @@ -# events-backend-module-aws-sqs +# `@backstage/plugins-events-backend-module-aws-sqs` -Welcome to the `events-backend-module-aws-sqs` backend plugin! +Welcome to the `events-backend-module-aws-sqs` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AwsSqsConsumingEventPublisher`. +This package is a module for the `events-backend` backend plugin +and extends the events system with an `AwsSqsConsumingEventPublisher`. -This event publisher will allow you to receive events from -an AWS SQS queue and will publish these to the used event broker. +This event publisher will allow you to receive events from an AWS SQS queue +and will publish these to the used `EventsService` implementation. ## Configuration @@ -32,15 +32,43 @@ events: ## Installation -1. Install the [`events-backend` plugin](../events-backend/README.md). -2. Install this module -3. Add your configuration. +1. Install this module +2. Add your configuration. ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-aws-sqs ``` -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); ``` + +### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +// ... +import { AwsSqsConsumingEventPublisher } from '@backstage/plugin-events-backend-module-aws-sqs'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const eventsRouter = Router(); + + // ... + + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); + await Promise.all(sqs.map(publisher => publisher.start())); + + return eventsRouter; +} +``` diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md index 61b3b63175..7c3920d499 100644 --- a/plugins/events-backend-module-azure/README.md +++ b/plugins/events-backend-module-azure/README.md @@ -1,9 +1,9 @@ # events-backend-module-azure -Welcome to the `events-backend-module-azure` backend plugin! +Welcome to the `events-backend-module-azure` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `AzureDevOpsEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `AzureDevOpsEventRouter`. The event router will subscribe to the topic `azureDevOps` and route the events to more concrete topics based on the value @@ -22,30 +22,22 @@ and [webhooks](https://learn.microsoft.com/en-us/azure/devops/service-hooks/serv ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-azure ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-azure/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const azureEventRouter = new AzureDevOpsEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(azureEventRouter) -+ .addSubscribers(azureEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new AzureDevOpsEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-bitbucket-cloud/README.md b/plugins/events-backend-module-bitbucket-cloud/README.md index 0a40ab2eea..7ff743c06d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/README.md +++ b/plugins/events-backend-module-bitbucket-cloud/README.md @@ -1,9 +1,9 @@ # events-backend-module-bitbucket-cloud -Welcome to the `events-backend-module-bitbucket-cloud` backend plugin! +Welcome to the `events-backend-module-bitbucket-cloud` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `BitbucketCloudEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `BitbucketCloudEventRouter`. The event router will subscribe to the topic `bitbucketCloud` and route the events to more concrete topics based on the value @@ -22,32 +22,24 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-bitbucket-cloud ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add( import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'), ); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const bitbucketCloudEventRouter = new BitbucketCloudEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(bitbucketCloudEventRouter) -+ .addSubscribers(bitbucketCloudEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new BitbucketCloudEventRouter({ + events: env.events, +}); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-gerrit/README.md b/plugins/events-backend-module-gerrit/README.md index b658fba366..d5b9f9683a 100644 --- a/plugins/events-backend-module-gerrit/README.md +++ b/plugins/events-backend-module-gerrit/README.md @@ -1,8 +1,8 @@ # events-backend-module-gerrit -Welcome to the `events-backend-module-gerrit` backend plugin! +Welcome to the `events-backend-module-gerrit` backend module! -This plugin is a module for the `events-backend` backend plugin +This package is a module for the `events-backend` backend plugin and extends it with an `GerritEventRouter`. The event router will subscribe to the topic `gerrit` @@ -21,30 +21,20 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gerrit ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend-module-gerrit/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: - -```diff -+const gerritEventRouter = new GerritEventRouter(); - -new EventsBackend(env.logger) -+ .addPublishers(gerritEventRouter) -+ .addSubscribers(gerritEventRouter); -// [...] +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GerritEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` diff --git a/plugins/events-backend-module-github/README.md b/plugins/events-backend-module-github/README.md index 072877ee86..fec27b62e3 100644 --- a/plugins/events-backend-module-github/README.md +++ b/plugins/events-backend-module-github/README.md @@ -1,9 +1,9 @@ # events-backend-module-github -Welcome to the `events-backend-module-github` backend plugin! +Welcome to the `events-backend-module-github` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GithubEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GithubEventRouter`. The event router will subscribe to the topic `github` and route the events to more concrete topics based on the value @@ -22,37 +22,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-github ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const githubEventRouter = new GithubEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubEventRouter } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(githubEventRouter) -+ .addSubscribers(githubEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GithubEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Signature Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGithubWebhook } from '@backstage/plugin-events-backend-module-github/alpha'; +// ... +backend.add(eventsModuleGithubWebhook()); +``` + +#### Legacy Backend System + Add the signature validator for the topic `github`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGithubSignatureValidator } from '@backstage/plugin-events-backend-module-github'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + github: { + validator: createGithubSignatureValidator(env.config), + }, @@ -61,7 +73,7 @@ Add the signature validator for the topic `github`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-module-gitlab/README.md b/plugins/events-backend-module-gitlab/README.md index 3d4919302f..73d5bf2d87 100644 --- a/plugins/events-backend-module-gitlab/README.md +++ b/plugins/events-backend-module-gitlab/README.md @@ -1,9 +1,9 @@ # events-backend-module-gitlab -Welcome to the `events-backend-module-gitlab` backend plugin! +Welcome to the `events-backend-module-gitlab` backend module! -This plugin is a module for the `events-backend` backend plugin -and extends it with an `GitlabEventRouter`. +This package is a module for the `events-backend` backend plugin +and extends the event system with an `GitlabEventRouter`. The event router will subscribe to the topic `gitlab` and route the events to more concrete topics based on the value @@ -21,37 +21,49 @@ Please find all possible webhook event types at the ## Installation -Install the [`events-backend` plugin](../events-backend/README.md). - -Install this module: - ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-events-backend-module-gitlab ``` -Add the event router to the `EventsBackend` instance in `packages/backend/src/plugins/events.ts`: +### Event Router -```diff -+const gitlabEventRouter = new GitlabEventRouter(); +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabEventRouter } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabEventRouter()); +``` -new EventsBackend(env.logger) -+ .addPublishers(gitlabEventRouter) -+ .addSubscribers(gitlabEventRouter); -// [...] +#### Legacy Backend System + +```ts +// packages/backend/src/plugins/events.ts +const eventRouter = new GitlabEventRouter({ events: env.events }); +await eventRouter.subscribe(); ``` ### Token Validator +```ts +// packages/backend/src/index.ts +import { eventsModuleGitlabWebhook } from '@backstage/plugin-events-backend-module-gitlab/alpha'; +// ... +backend.add(eventsModuleGitlabWebhook()); +``` + +#### Legacy Backend System + Add the token validator for the topic `gitlab`: ```diff -// at packages/backend/src/plugins/events.ts +// packages/backend/src/plugins/events.ts + import { createGitlabTokenValidator } from '@backstage/plugin-events-backend-module-gitlab'; -// [...] - const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { + // [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + gitlab: { + validator: createGitlabTokenValidator(env.config), + }, @@ -60,7 +72,7 @@ Add the token validator for the topic `gitlab`: }); ``` -Additionally, you need to add the configuration: +## Configuration ```yaml events: diff --git a/plugins/events-backend-test-utils/README.md b/plugins/events-backend-test-utils/README.md index c8727b536b..84a1be754e 100644 --- a/plugins/events-backend-test-utils/README.md +++ b/plugins/events-backend-test-utils/README.md @@ -1,4 +1,5 @@ -# plugin-events-backend-test-utils +# `@backstage/plugin-events-backend-test-utils` -Houses implementations of plugin-events-node interfaces -which can be useful for test for events-backend and its modules. +This is a package that can be used as `devDependency` +and provides a test implementation for the `EventsService` +by [`events-node` package](../events-node/README.md): `TestEventsService`. diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index b8470101aa..2dba259986 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -1,166 +1,55 @@ -# events-backend +# `@backstage/plugin-events-backend` Welcome to the events-backend backend plugin! -This plugin provides the wiring of all extension points -for managing events as defined by [plugin-events-node](../events-node) -including backend plugin `EventsPlugin` and `EventsBackend`. - -Additionally, it uses a simple in-process implementation for -the `EventBroker` by default which you can replace with a more sophisticated -implementation of your choice as you need (e.g., via module). - -Some of these (non-exhaustive) may provide added persistence, -or use external systems like AWS EventBridge, AWS SNS, Kafka, etc. +This package is based on [events-node](../events-node) and its `eventsServiceRef` +that is at the core of the event support. +It provides an `eventsPlugin` (exported as `default`). By default, the plugin ships with support to receive events via HTTP endpoints -`POST /api/events/http/{topic}` and will publish these -to the used event broker. +`POST /api/events/http/{topic}` and will publish these to the `EventsService`. + +HTTP ingresses can be enabled by config, or using the extension point +of the `eventsPlugin`. +Additionally, the latter allows to add a request validator +(e.g., signature verification). ## Installation ```bash # From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-events-backend @backstage/plugin-events-node +yarn --cwd packages/backend add @backstage/plugin-events-backend ``` -### Add to backend - -```ts title="packages/backend/src/index.ts" +```ts +// packages/backend/src/index.ts backend.add(import('@backstage/plugin-events-backend/alpha')); ``` -### Add to backend (old) +### Legacy Backend System -#### Event Broker +```ts +// packages/backend/src/plugins/events.ts +import { HttpPostIngressEventPublisher } from '@backstage/plugin-events-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; -First you will need to add and implementation of the `EventBroker` interface to the backend plugin environment. -This will allow event broker instance any backend plugins to publish and subscribe to events in order to communicate -between them. - -Add the following to `makeCreateEnv` - -```diff -// packages/backend/src/index.ts -+ import { DefaultEventBroker } from '@backstage/plugin-events-backend'; -+ const eventBroker = new DefaultEventBroker(root.child({ type: 'plugin' })); -``` - -Then update plugin environment to include the event broker. - -```diff -// packages/backend/src/types.ts -+ import { EventBroker } from '@backstage/plugin-events-node'; -+ eventBroker: EventBroker; -``` - -#### Publishing and Subscribing to events with the broker - -Backend plugins are passed the event broker in the plugin environment at startup of the application. The plugin can -make use of this to communicate between parts of the application. - -Here is an example of a plugin publishing a payload to a topic. - -```typescript jsx export default async function createPlugin( env: PluginEnvironment, ): Promise { - env.eventBroker.publish({ - topic: 'publish.example', - eventPayload: { message: 'Hello, World!' }, - metadata: {}, + const eventsRouter = Router(); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + logger: env.logger, }); + http.bind(eventsRouter); + + return eventsRouter; } ``` -Here is an example of a plugin subscribing to a topic. - -```typescript jsx -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - env.eventBroker.subscribe([ - { - supportsEventTopics: ['publish.example'], - onEvent: async (params: EventParams) => { - env.logger.info(`receieved ${params.topic} event`); - }, - }, - ]); -} -``` - -#### Implementing an `EventSubscriber` class - -More complex solutions might need the creation of a class that implements the `EventSubscriber` interface. e.g. - -```typescript jsx -import { EventSubscriber } from './EventSubscriber'; - -class ExampleSubscriber implements EventSubscriber { - // ... - - supportsEventTopics() { - return ['publish.example']; - } - - async onEvent(params: EventParams) { - env.logger.info(`receieved ${params.topic} event`); - } -} -``` - -#### Events Backend - -The events backend plugin provides a router to handler http events and publish the http requests onto the event -broker. - -To configure it add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) -to your Backstage project. - -Additionally, add the events plugin to your backend. - -```diff -// packages/backend/src/index.ts -// [...] -+import events from './plugins/events'; -// [...] -+ const eventsEnv = useHotMemoize(module, () => createEnv('events')); -// [...] -+ apiRouter.use('/events', await events(eventsEnv)); -// [...] -``` - -#### Configuration - -In order to create HTTP endpoints to receive events for a certain -topic, you need to add them at your configuration: - -```yaml -events: - http: - topics: - - bitbucketCloud - - github - - whatever -``` - -Only those topics added to the configuration will result in -available endpoints. - -The example above would result in the following endpoints: - -``` -POST /api/events/http/bitbucketCloud -POST /api/events/http/github -POST /api/events/http/whatever -``` - -You may want to use these for webhooks by SCM providers -in combination with suitable event subscribers. - -However, it is not limited to these use cases. - ### Event-based Entity Providers You can implement the `EventSubscriber` interface on an `EntityProviders` to allow it to handle events from other plugins e.g. the event backend plugin @@ -189,74 +78,42 @@ Assuming you have configured the `eventBroker` into the `PluginEnvironment` you } ``` +## Configuration + +In order to create HTTP endpoints to receive events for a certain +topic, you need to add them at your configuration: + +```yaml +events: + http: + topics: + - bitbucketCloud + - github + - whatever +``` + +Only those topics added to the configuration will result in +available endpoints. + +The example above would result in the following endpoints: + +``` +POST /api/events/http/bitbucketCloud +POST /api/events/http/github +POST /api/events/http/whatever +``` + +You may want to use these for webhooks by SCM providers +in combination with suitable event subscribers. + +However, it is not limited to these use cases. + ## Use Cases -### Custom Event Broker - -Example using the `EventsBackend`: - -```ts -new EventsBackend(env.logger) - .setEventBroker(yourEventBroker) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; - -// [...] - -export const yourModuleEventsModule = createBackendModule({ - pluginId: 'events', - moduleId: 'your-module', - register(env) { - // [...] - env.registerInit({ - deps: { - // [...] - events: eventsExtensionPoint, - // [...] - }, - async init({ /* ... */ events /*, ... */ }) { - // [...] - const yourEventBroker = new YourEventBroker(); - // [...] - events.setEventBroker(yourEventBroker); - }, - }); - }, -}); -``` - ### Request Validator -Example using the `EventsBackend`: - ```ts -const http = HttpPostIngressEventPublisher.fromConfig({ - config: env.config, - ingresses: { - yourTopic: { - validator: yourValidator, - }, - }, - logger: env.logger, -}); -http.bind(router); - -await new EventsBackend(env.logger) - .addPublishers(http) - // [...] - .start(); -``` - -Example using a module: - -```ts -import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; // [...] @@ -282,3 +139,19 @@ export const eventsModuleYourFeature = createBackendModule({ }, }); ``` + +#### Legacy Backend System + +```ts +const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + events: env.events, + ingresses: { + yourTopic: { + validator: yourValidator, + }, + }, + logger: env.logger, +}); +http.bind(router); +``` diff --git a/plugins/events-node/README.md b/plugins/events-node/README.md index 44738222bc..ce4d705025 100644 --- a/plugins/events-node/README.md +++ b/plugins/events-node/README.md @@ -1,3 +1,82 @@ -# plugin-events-node +# `@backstage/plugin-events-node` -Houses types and utilities for building events-related modules. +This package defined basic types for event-based interactions inside of Backstage. + +Additionally, it provides the core event service `eventsServiceRef` of type `EventsService` +with its default implementation that uses the `DefaultEventsService` implementation. + +`DefaultEventsService` is a simple in-memory implementation +that requires the co-deployment of producers and consumers of events. + +## Installation + +Add `@backstage/plugin-events-node` as dependency to your plugin or plugin module package +to which you want to add event support. + +Use `eventsServiceRef` as a dependency at your plugin or plugin module. + +### Legacy Backend System + +Create an `EventsService` instance and add it to the environment. + +```ts +// packages/backend/src/plugins/events.ts +import { DefaultEventsService } from '@backstage/plugin-events-node'; + +// ... + +function makeCreateEnv(config: Config) { + // ... + const eventsService = DefaultEventsService.create({ logger: root }); + // ... + return (plugin: string): PluginEnvironment => { + // ... + return { + // ... + events: eventsService, + // ... + }; + }; +} +``` + +Use the `events` from the `PluginEnvironment` as desired: + +```ts +// packages/backend/src/plugins/events.ts +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // ... + env.events; // ... + // ... +} +``` + +## Use Case + +### Exchange service implementation + +Create your custom service factory implementation: + +```ts +import { eventsServiceRef } from '@backstage/plugin-events-node'; +// ... +export const customEventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: { + // add needed dependencies here + }, + async factory({ logger }) { + // add your custom logic here + return customEventsService; + }, +}); +``` + +and your custom implementation: + +```diff +// packages/backend/src/index.ts ++ backend.add(customEventsServiceFactory()); +``` From 9e527c920065fc015f2b6a045079545d2e6961c1 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Tue, 23 Jan 2024 22:44:32 +0100 Subject: [PATCH 336/483] fix(events,catalog,bitbucket-cloud)!: fix new backend system support; migrates to `EventsService` - Fixes the support for the new backend system that was broken entirely (with and without events support). - Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`. Signed-off-by: Patrick Jungermann --- .changeset/silver-flowers-trade.md | 40 +++++++++++++++ docs/integrations/bitbucketCloud/discovery.md | 33 ++++++++++++- .../api-report.md | 18 +++---- .../package.json | 4 +- ...ModuleBitbucketCloudEntityProvider.test.ts | 32 +++++++----- ...talogModuleBitbucketCloudEntityProvider.ts | 12 ++--- .../BitbucketCloudEntityProvider.test.ts | 24 ++++----- .../providers/BitbucketCloudEntityProvider.ts | 49 ++++++++++--------- yarn.lock | 2 +- 9 files changed, 141 insertions(+), 73 deletions(-) create mode 100644 .changeset/silver-flowers-trade.md diff --git a/.changeset/silver-flowers-trade.md b/.changeset/silver-flowers-trade.md new file mode 100644 index 0000000000..8701de375c --- /dev/null +++ b/.changeset/silver-flowers-trade.md @@ -0,0 +1,40 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor +--- + +BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support. + +`BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`. +With provided `events`, the event-based updates/refresh will be available. +However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`. + +The event subscription happens on `connect(connection)` if the `events` is available. + +**Migration:** + +```diff + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), ++ events: env.events, + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, + }, + ); +- env.eventBroker.subscribe(bitbucketCloudProvider); +``` + +**New Backend System:** + +Before this change, using this module with the new backend system was broken. +Now, you can add the catalog module for Bitbucket Cloud incl. event support backend. +Event support will always be enabled. +However, no updates/refresh will happen without receiving events. + +```ts +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), +); +``` diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index a7477a2a61..81770afbf8 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -24,7 +24,35 @@ package. yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` -### Installation without Events Support +### Installation with New Backend System + +```ts +// optional if you want HTTP endpojnts to receive external events +// backend.add(import('@backstage/plugin-events-backend/alpha')); +// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events +// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs/alpha')); +backend.add( + import('@backstage/plugin-events-backend-module-bitbucket-cloud/alpha'), +); +backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), +); +``` + +You need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Further documentation: + +- +- +- + +### Installation with Legacy Backend System + +#### Installation without Events Support And then add the entity provider to your catalog builder: @@ -49,7 +77,7 @@ export default async function createPlugin( } ``` -### Installation with Events Support +#### Installation with Events Support Please follow the installation instructions at @@ -83,6 +111,7 @@ export default async function createPlugin( env.config, { catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + events: env.events, logger: env.logger, scheduler: env.scheduler, tokenManager: env.tokenManager, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index 22eeddea3a..22845cf260 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -7,18 +7,15 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; -import { EventParams } from '@backstage/plugin-events-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; -import { EventSubscriber } from '@backstage/plugin-events-node'; -import { Logger } from 'winston'; +import { EventsService } from '@backstage/plugin-events-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; // @public -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -26,7 +23,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -37,12 +35,8 @@ export class BitbucketCloudEntityProvider // (undocumented) getTaskId(): string; // (undocumented) - onEvent(params: EventParams): Promise; - // (undocumented) onRepoPush(event: Events.RepoPushEvent): Promise; // (undocumented) - refresh(logger: Logger): Promise; - // (undocumented) - supportsEventTopics(): string[]; + refresh(logger: LoggerService): Promise; } ``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 418e88a003..441781331e 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -56,13 +56,13 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", - "uuid": "^9.0.0", - "winston": "^3.2.1" + "uuid": "^9.0.0" }, "devDependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", "luxon": "^3.0.0", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts index c771a40acc..9fff322eab 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts @@ -14,18 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { Duration } from 'luxon'; import { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; describe('catalogModuleBitbucketCloudEntityProvider', () => { it('should register provider at the catalog extension point', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); let addedProviders: Array | undefined; - let addedSubscribers: Array | undefined; let usedSchedule: TaskScheduleDefinition | undefined; const catalogExtensionPointImpl = { @@ -33,11 +43,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { addedProviders = providers; }, }; - const eventsExtensionPointImpl = { - addSubscribers: (subscribers: any) => { - addedSubscribers = subscribers; - }, - }; + const connection = jest.fn() as unknown as EntityProviderConnection; const runner = jest.fn(); const scheduler = mockServices.scheduler.mock({ createScheduledTaskRunner(schedule) { @@ -49,9 +55,9 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, catalogExtensionPointImpl], - [eventsExtensionPoint, eventsExtensionPointImpl], ], features: [ + eventsServiceFactory(), catalogModuleBitbucketCloudEntityProvider(), mockServices.rootConfig.factory({ data: { @@ -75,10 +81,14 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); expect(addedProviders?.length).toEqual(1); - expect(addedProviders?.pop()?.getProviderName()).toEqual( + expect(runner).not.toHaveBeenCalled(); + const provider = addedProviders!.pop()!; + expect(provider.getProviderName()).toEqual( 'bitbucketCloud-provider:default', ); - expect(addedSubscribers).toEqual(addedProviders); - expect(runner).not.toHaveBeenCalled(); + await provider.connect(connection); + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('bitbucketCloud-provider:default'); + expect(runner).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts index 39265dc1a5..21d86ce8e6 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, createBackendModule, @@ -23,7 +22,7 @@ import { catalogProcessingExtensionPoint, catalogServiceRef, } from '@backstage/plugin-catalog-node/alpha'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; /** @@ -38,9 +37,7 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ catalog: catalogProcessingExtensionPoint, catalogApi: catalogServiceRef, config: coreServices.rootConfig, - // TODO(pjungermann): How to make this optional for those which only want the provider without event support? - // Do we even want to support this? - events: eventsExtensionPoint, + events: eventsServiceRef, logger: coreServices.logger, scheduler: coreServices.scheduler, tokenManager: coreServices.tokenManager, @@ -54,16 +51,15 @@ export const catalogModuleBitbucketCloudEntityProvider = createBackendModule({ scheduler, tokenManager, }) { - const winstonLogger = loggerToWinstonLogger(logger); const providers = BitbucketCloudEntityProvider.fromConfig(config, { catalogApi, - logger: winstonLogger, + events, + logger, scheduler, tokenManager, }); catalog.addEntityProvider(providers); - events.addSubscribers(providers); }, }); }, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 573f21ce60..6a21d95afa 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -29,6 +29,7 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { Events } from '@backstage/plugin-bitbucket-cloud-common'; +import { DefaultEventsService } from '@backstage/plugin-events-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { @@ -436,6 +437,7 @@ describe('BitbucketCloudEntityProvider', () => { 'added-module/catalog-custom.yaml', ); + const events = DefaultEventsService.create({ logger }); const catalogApi = { getEntities: async ( request: { filter: Record }, @@ -457,6 +459,7 @@ describe('BitbucketCloudEntityProvider', () => { }; const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, @@ -537,7 +540,7 @@ describe('BitbucketCloudEntityProvider', () => { ); await provider.connect(entityProviderConnection); - await provider.onEvent(repoPushEventParams); + await events.publish(repoPushEventParams); const addedEntities = [ { @@ -566,31 +569,22 @@ describe('BitbucketCloudEntityProvider', () => { }); }); - it('onRepoPush fail on incomplete setup', async () => { - const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - logger, - schedule, - })[0]; - - await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow( - 'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.', - ); - }); - it('no onRepoPush update on non-matching workspace slug', async () => { const catalogApi = { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, @@ -613,15 +607,17 @@ describe('BitbucketCloudEntityProvider', () => { getEntities: jest.fn(), refreshEntity: jest.fn(), }; + const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { catalogApi: catalogApi as any as CatalogApi, + events, logger, schedule, tokenManager, })[0]; await provider.connect(entityProviderConnection); - await provider.onEvent({ + await events.publish({ ...repoPushEventParams, eventPayload: { ...repoPushEventParams.eventPayload, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts index 14f561473b..9135e0717f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.ts @@ -15,6 +15,7 @@ */ import { TokenManager } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; import { LocationEntity } from '@backstage/catalog-model'; @@ -35,13 +36,12 @@ import { locationSpecToLocationEntity, } from '@backstage/plugin-catalog-node'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProviderConfig, readProviderConfigs, } from './BitbucketCloudEntityProviderConfig'; import * as uuid from 'uuid'; -import { Logger } from 'winston'; const DEFAULT_BRANCH = 'master'; const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push'; @@ -62,14 +62,13 @@ interface IngestionTarget { * * @public */ -export class BitbucketCloudEntityProvider - implements EntityProvider, EventSubscriber -{ +export class BitbucketCloudEntityProvider implements EntityProvider { private readonly client: BitbucketCloudClient; private readonly config: BitbucketCloudEntityProviderConfig; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly scheduleFn: () => Promise; private readonly catalogApi?: CatalogApi; + private readonly events?: EventsService; private readonly tokenManager?: TokenManager; private connection?: EntityProviderConnection; @@ -79,7 +78,8 @@ export class BitbucketCloudEntityProvider config: Config, options: { catalogApi?: CatalogApi; - logger: Logger; + events?: EventsService; + logger: LoggerService; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; tokenManager?: TokenManager; @@ -114,6 +114,7 @@ export class BitbucketCloudEntityProvider options.logger, taskRunner, options.catalogApi, + options.events, options.tokenManager, ); }); @@ -122,9 +123,10 @@ export class BitbucketCloudEntityProvider private constructor( config: BitbucketCloudEntityProviderConfig, integration: BitbucketCloudIntegration, - logger: Logger, + logger: LoggerService, taskRunner: TaskRunner, catalogApi?: CatalogApi, + events?: EventsService, tokenManager?: TokenManager, ) { this.client = BitbucketCloudClient.fromConfig(integration.config); @@ -134,6 +136,7 @@ export class BitbucketCloudEntityProvider }); this.scheduleFn = this.createScheduleFn(taskRunner); this.catalogApi = catalogApi; + this.events = events; this.tokenManager = tokenManager; } @@ -176,9 +179,23 @@ export class BitbucketCloudEntityProvider async connect(connection: EntityProviderConnection): Promise { this.connection = connection; await this.scheduleFn(); + + if (this.events) { + await this.events.subscribe({ + id: this.getProviderName(), + topics: [TOPIC_REPO_PUSH], + onEvent: async params => { + if (params.topic !== TOPIC_REPO_PUSH) { + return; + } + + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); + }, + }); + } } - async refresh(logger: Logger) { + async refresh(logger: LoggerService) { if (!this.connection) { throw new Error('Not initialized'); } @@ -198,20 +215,6 @@ export class BitbucketCloudEntityProvider ); } - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ - supportsEventTopics(): string[] { - return [TOPIC_REPO_PUSH]; - } - - /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ - async onEvent(params: EventParams): Promise { - if (params.topic !== TOPIC_REPO_PUSH) { - return; - } - - await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); - } - private canHandleEvents(): boolean { if (this.catalogApi && this.tokenManager) { return true; diff --git a/yarn.lock b/yarn.lock index a876413ec3..5439b536e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5303,11 +5303,11 @@ __metadata: "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 uuid: ^9.0.0 - winston: ^3.2.1 languageName: unknown linkType: soft From ff33ee2ef42b3c73bad18eb41c718d8ceb8eb948 Mon Sep 17 00:00:00 2001 From: Harrison Hogg Date: Mon, 26 Feb 2024 11:46:00 +0000 Subject: [PATCH 337/483] Removed hardcoded font-family on select input Signed-off-by: Harrison Hogg --- .changeset/five-beers-accept.md | 5 +++++ packages/core-components/src/components/Select/Select.tsx | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/five-beers-accept.md diff --git a/.changeset/five-beers-accept.md b/.changeset/five-beers-accept.md new file mode 100644 index 0000000000..d5dbcf88d7 --- /dev/null +++ b/.changeset/five-beers-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed hardcoded font-family on select input diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index ec7f87eb94..b1a2139b85 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -55,7 +55,6 @@ const BootstrapInput = withStyles( fontSize: theme.typography.body1.fontSize, padding: theme.spacing(1.25, 3.25, 1.25, 1.5), transition: theme.transitions.create(['border-color', 'box-shadow']), - fontFamily: 'Helvetica Neue', '&:focus': { background: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, From 5e639219f7bfd31421703953eba26433b5e39b95 Mon Sep 17 00:00:00 2001 From: Axel Koehler Date: Mon, 26 Feb 2024 13:06:56 +0100 Subject: [PATCH 338/483] Fix typo in extending model docs Signed-off-by: Axel Koehler --- docs/features/software-catalog/extending-the-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index fec924fa91..72b7c20e07 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -451,7 +451,7 @@ You can generate an isomorphic plugin package by running:`yarn new --select plug or you can run `yarn new` and then select "plugin-common" from the list of options There's at this point no existing templates for generating isomorphic plugins -using the `@backstage/cli`. Perhaps the simplest wat to get started right now is +using the `@backstage/cli`. Perhaps the simplest way to get started right now is to copy the contents of one of the existing packages in the main repository, such as `plugins/scaffolder-common`, and rename the folder and file contents to the desired name. This example uses _foobar_ as the plugin name so the plugin From 27177d74648a590f9887f55b6fde10aa3a0bf1a9 Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Mon, 26 Feb 2024 13:55:25 +0100 Subject: [PATCH 339/483] adds the opa permissions wrapper plugin to the microsite Signed-off-by: Peter Macdonald --- .../data/plugins/opa-permissions-wrapper.yaml | 10 ++++++++++ microsite/static/img/opapermlogo.png | Bin 0 -> 585867 bytes 2 files changed, 10 insertions(+) create mode 100644 microsite/data/plugins/opa-permissions-wrapper.yaml create mode 100644 microsite/static/img/opapermlogo.png diff --git a/microsite/data/plugins/opa-permissions-wrapper.yaml b/microsite/data/plugins/opa-permissions-wrapper.yaml new file mode 100644 index 0000000000..64af859dbc --- /dev/null +++ b/microsite/data/plugins/opa-permissions-wrapper.yaml @@ -0,0 +1,10 @@ +--- +title: OPA Permissions Wrapper +author: Peter Macdonald +authorUrl: https://github.com/Parsifal-M +category: Authentication/Authorization +description: Manage your Backstage permissions with OPA (Open Policy Agent)! +documentation: https://github.com/Parsifal-M/backstage-opa-plugins/blob/main/plugins/permission-backend-module-opa-wrapper/README.md +iconUrl: /img/opapermlogo.png +npmPackageName: '@parsifal-m/plugin-permission-backend-module-opa-wrapper' +addedDate: '2024-02-26' diff --git a/microsite/static/img/opapermlogo.png b/microsite/static/img/opapermlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..703e53dbf297c2e892b31685eb1b1a777eb90e3c GIT binary patch literal 585867 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≦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⪼ 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�lJOTs*A}-NYFLG+`Zcov~~ex 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 z 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 Date: Mon, 26 Feb 2024 14:04:02 +0000 Subject: [PATCH 340/483] Update changeset from minor to patch. Signed-off-by: Phill Morton Signed-off-by: Phillip Morton --- .changeset/forty-oranges-joke.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/forty-oranges-joke.md b/.changeset/forty-oranges-joke.md index fb08927fa0..28d162442c 100644 --- a/.changeset/forty-oranges-joke.md +++ b/.changeset/forty-oranges-joke.md @@ -1,5 +1,5 @@ --- -'@backstage/integration-react': minor +'@backstage/integration-react': patch --- Updated `microsoftAuthApi` scopes for Azure DevOps to be fully qualified. From 930b5c197ad2dd3185045b06d2464732a97ffec3 Mon Sep 17 00:00:00 2001 From: Harrison Hogg Date: Mon, 26 Feb 2024 13:39:00 +0000 Subject: [PATCH 341/483] Added root and label class keys for autocomplete pickers Signed-off-by: Harrison Hogg --- .changeset/hungry-points-burn.md | 5 +++ .changeset/pretty-boats-promise.md | 5 +++ plugins/catalog-react/api-report.md | 11 ++++-- .../EntityAutocompletePicker.tsx | 19 ++++++++-- .../EntityAutocompletePicker/index.ts | 1 + .../EntityOwnerPicker/EntityOwnerPicker.tsx | 8 +++-- .../EntityProcessingStatusPicker.tsx | 11 ++++-- .../src/overridableComponents.ts | 2 ++ plugins/scaffolder-react/api-report-alpha.md | 17 +++++++++ .../TemplateCategoryPicker.tsx | 16 ++++++++- .../TemplateCategoryPicker/index.ts | 1 + plugins/scaffolder-react/src/next/index.ts | 1 + .../src/next/overridableComponents.ts | 36 +++++++++++++++++++ 13 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 .changeset/hungry-points-burn.md create mode 100644 .changeset/pretty-boats-promise.md create mode 100644 plugins/scaffolder-react/src/next/overridableComponents.ts diff --git a/.changeset/hungry-points-burn.md b/.changeset/hungry-points-burn.md new file mode 100644 index 0000000000..c0dabf0446 --- /dev/null +++ b/.changeset/hungry-points-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added 'root' and 'label' class keys for EntityAutocompletePicker, EntityOwnerPicker and EntityProcessingStatusPicker diff --git a/.changeset/pretty-boats-promise.md b/.changeset/pretty-boats-promise.md new file mode 100644 index 0000000000..751ccaf14c --- /dev/null +++ b/.changeset/pretty-boats-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Added 'root' and 'label' class key to TemplateCategoryPicker diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 62bdf3f2c9..a492a72e9f 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -93,8 +93,12 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; + CatalogReactEntityAutocompletePickerClassKey: CatalogReactEntityAutocompletePickerClassKey; }; +// @public (undocumented) +export type CatalogReactEntityAutocompletePickerClassKey = 'root' | 'label'; + // @public export type CatalogReactEntityDisplayNameClassKey = 'root' | 'icon'; @@ -105,10 +109,13 @@ export type CatalogReactEntityLifecyclePickerClassKey = 'input'; export type CatalogReactEntityNamespacePickerClassKey = 'input'; // @public (undocumented) -export type CatalogReactEntityOwnerPickerClassKey = 'input'; +export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; // @public (undocumented) -export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; +export type CatalogReactEntityProcessingStatusPickerClassKey = + | 'input' + | 'root' + | 'label'; // @public (undocumented) export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 98e680aa12..1b7539b912 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Box, TextFieldProps, Typography } from '@material-ui/core'; +import { Box, TextFieldProps, Typography, makeStyles } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; @@ -52,6 +52,17 @@ export type EntityAutocompletePickerProps< initialSelectedOptions?: string[]; }; +/** @public */ +export type CatalogReactEntityAutocompletePickerClassKey = 'root' | 'label'; + +const useStyles = makeStyles( + { + root: {}, + label: {}, + }, + { name: 'CatalogReactEntityAutocompletePicker' }, +); + /** @public */ export function EntityAutocompletePicker< T extends DefaultEntityFilters = DefaultEntityFilters, @@ -67,6 +78,8 @@ export function EntityAutocompletePicker< initialSelectedOptions = [], } = props; + const classes = useStyles(); + const { updateFilters, filters, @@ -127,8 +140,8 @@ export function EntityAutocompletePicker< if (availableOptions.length <= 1) return null; return ( - - + + {label} multiple diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts index 684e7cef55..87811be894 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/index.ts @@ -16,6 +16,7 @@ export { EntityAutocompletePicker } from './EntityAutocompletePicker'; export type { + CatalogReactEntityAutocompletePickerClassKey, EntityAutocompletePickerProps, AllowedEntityFilters, } from './EntityAutocompletePicker'; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 5b5bd39310..87de6411b2 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -44,10 +44,12 @@ import { withStyles } from '@material-ui/core/styles'; import { useEntityPresentation } from '../../apis'; /** @public */ -export type CatalogReactEntityOwnerPickerClassKey = 'input'; +export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; const useStyles = makeStyles( { + root: {}, + label: {}, input: {}, fullWidth: { width: '100%' }, boxLabel: { @@ -174,8 +176,8 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { } return ( - - + + Owner { const availableAdvancedItems = ['Is Orphan', 'Has Error']; return ( - - + + Processing Status multiple diff --git a/plugins/catalog-react/src/overridableComponents.ts b/plugins/catalog-react/src/overridableComponents.ts index 6424c95ab5..b45fd30103 100644 --- a/plugins/catalog-react/src/overridableComponents.ts +++ b/plugins/catalog-react/src/overridableComponents.ts @@ -26,6 +26,7 @@ import { CatalogReactEntityOwnerPickerClassKey, CatalogReactEntityProcessingStatusPickerClassKey, } from './components'; +import { CatalogReactEntityAutocompletePickerClassKey } from './components/EntityAutocompletePicker/EntityAutocompletePicker'; /** @public */ export type CatalogReactComponentsNameToClassKey = { @@ -36,6 +37,7 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; + CatalogReactEntityAutocompletePickerClassKey: CatalogReactEntityAutocompletePickerClassKey; }; /** @public */ diff --git a/plugins/scaffolder-react/api-report-alpha.md b/plugins/scaffolder-react/api-report-alpha.md index a521fecc70..683b8cf8be 100644 --- a/plugins/scaffolder-react/api-report-alpha.md +++ b/plugins/scaffolder-react/api-report-alpha.md @@ -16,6 +16,7 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; +import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; @@ -25,6 +26,7 @@ import { ScaffolderRJSFFormProps } from '@backstage/plugin-scaffolder-react'; import { ScaffolderStep } from '@backstage/plugin-scaffolder-react'; import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react'; import { SetStateAction } from 'react'; +import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; @@ -32,6 +34,13 @@ import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react'; import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UiSchema } from '@rjsf/utils'; +// @alpha (undocumented) +export type BackstageOverrides = Overrides & { + [Name in keyof ScaffolderReactComponentsNameToClassKey]?: Partial< + StyleRules + >; +}; + // @alpha (undocumented) export const createAsyncValidators: ( rootSchema: JsonObject, @@ -132,6 +141,14 @@ export type ScaffolderPageContextMenuProps = { onCreateClicked?: () => void; }; +// @alpha (undocumented) +export type ScaffolderReactComponentsNameToClassKey = { + ScaffolderReactTemplateCategoryPicker: ScaffolderReactTemplateCategoryPickerClassKey; +}; + +// @alpha (undocumented) +export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; + // @alpha export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element; diff --git a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx index eba6aebf93..9c85cacfc2 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx @@ -23,6 +23,7 @@ import { FormControlLabel, TextField, Typography, + makeStyles, } from '@material-ui/core'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; @@ -34,12 +35,24 @@ import { alertApiRef, useApi } from '@backstage/core-plugin-api'; const icon = ; const checkedIcon = ; +/** @alpha */ +export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; + +const useStyles = makeStyles( + { + root: {}, + label: {}, + }, + { name: 'ScaffolderReactTemplateCategoryPicker' }, +); + /** * The Category Picker that is rendered on the left side for picking * categories and filtering the template list. * @alpha */ export const TemplateCategoryPicker = () => { + const classes = useStyles(); const alertApi = useApi(alertApiRef); const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); @@ -57,8 +70,9 @@ export const TemplateCategoryPicker = () => { if (!availableTypes) return null; return ( - + + >; +}; + +declare module '@backstage/theme' { + interface OverrideComponentNameToClassKeys + extends ScaffolderReactComponentsNameToClassKey {} +} From fb02400618e78b7f8c1c4a3e0423ca262a494562 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:24:22 +0000 Subject: [PATCH 342/483] fix(deps): update dependency mysql2 to v3.9.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5439b536e2..83f9132421 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35590,8 +35590,8 @@ __metadata: linkType: hard "mysql2@npm:^3.0.0": - version: 3.9.1 - resolution: "mysql2@npm:3.9.1" + version: 3.9.2 + resolution: "mysql2@npm:3.9.2" dependencies: denque: ^2.1.0 generate-function: ^2.3.1 @@ -35601,7 +35601,7 @@ __metadata: named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 067353f8735d3e91654ecc01f562729c87f4fa141e870d524af06c5db98ac3341d91f3130357fead247833f41b1b93d1c6dd6e1f1b98687d28998bd9673b9ce7 + checksum: a236a52659d67812af494bc41d09a2bd906d12755887ba75bbdf271c25ad5668030e0a2e91dc61ba4b3b225fb2b5a6aa07a168e077fcdaf50d6a73930c11de5d languageName: node linkType: hard From e998fb7889157311e0ebf3819b4abaf74c0c6a66 Mon Sep 17 00:00:00 2001 From: Harrison Hogg <7130591+HHogg@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:29:20 +0000 Subject: [PATCH 343/483] Update plugins/scaffolder-react/src/next/overridableComponents.ts Co-authored-by: Philipp Hugenroth Signed-off-by: Harrison Hogg <7130591+HHogg@users.noreply.github.com> --- plugins/scaffolder-react/src/next/overridableComponents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-react/src/next/overridableComponents.ts b/plugins/scaffolder-react/src/next/overridableComponents.ts index 0a97a18645..56986d4f80 100644 --- a/plugins/scaffolder-react/src/next/overridableComponents.ts +++ b/plugins/scaffolder-react/src/next/overridableComponents.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. From ce73c3b3ebec5a04ad9c59675ab03cd7352681cc Mon Sep 17 00:00:00 2001 From: Harrison Hogg Date: Mon, 26 Feb 2024 15:20:42 +0000 Subject: [PATCH 344/483] Removed inline color of select icon Signed-off-by: Harrison Hogg --- .changeset/modern-impalas-add.md | 5 +++++ .../src/components/Select/static/ClosedDropdown.tsx | 3 ++- .../src/components/Select/static/OpenedDropdown.tsx | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/modern-impalas-add.md diff --git a/.changeset/modern-impalas-add.md b/.changeset/modern-impalas-add.md new file mode 100644 index 0000000000..8964e07fea --- /dev/null +++ b/.changeset/modern-impalas-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed the inline color from select icon to allow it to be colored via a theme diff --git a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx index 812afe34eb..00c748ce29 100644 --- a/packages/core-components/src/components/Select/static/ClosedDropdown.tsx +++ b/packages/core-components/src/components/Select/static/ClosedDropdown.tsx @@ -27,6 +27,7 @@ const useStyles = makeStyles( position: 'absolute', right: theme.spacing(0.5), pointerEvents: 'none', + color: '#616161', }, }), { name: 'BackstageClosedDropdown' }, @@ -42,7 +43,7 @@ const ClosedDropdown = () => { > ); diff --git a/packages/core-components/src/components/Select/static/OpenedDropdown.tsx b/packages/core-components/src/components/Select/static/OpenedDropdown.tsx index b87a00c26a..4ca2a5a065 100644 --- a/packages/core-components/src/components/Select/static/OpenedDropdown.tsx +++ b/packages/core-components/src/components/Select/static/OpenedDropdown.tsx @@ -26,6 +26,7 @@ const useStyles = makeStyles( position: 'absolute', right: theme.spacing(0.5), pointerEvents: 'none', + color: '#616161', }, }), { name: 'BackstageOpenedDropdown' }, @@ -41,7 +42,7 @@ const OpenedDropdown = () => { > ); From a8d046319e52902f95bc8c0139239512c958b703 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 15:38:48 -0500 Subject: [PATCH 345/483] create a new guest auth provider. running into an issue on reload of an active state Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- app-config.yaml | 4 + packages/app-defaults/src/defaults/apis.ts | 17 +++ packages/app/src/App.tsx | 2 +- packages/app/src/identityProviders.ts | 7 ++ packages/backend/package.json | 1 + packages/backend/src/plugins/auth.ts | 2 + .../implementations/auth/guest/GuestAuth.ts | 113 ++++++++++++++++++ .../apis/implementations/auth/guest/index.ts | 16 +++ .../src/apis/implementations/auth/index.ts | 1 + .../src/apis/definitions/auth.ts | 12 ++ .../.eslintrc.js | 1 + .../README.md | 5 + .../package.json | 42 +++++++ .../src/createGuestAuthFactory.ts | 54 +++++++++ .../src/createGuestAuthRouteHandlers.ts | 111 +++++++++++++++++ .../src/index.ts | 25 ++++ .../src/module.ts | 44 +++++++ .../src/resolvers.ts | 40 +++++++ .../src/types.ts | 25 ++++ .../auth-backend/src/providers/guest/index.ts | 16 +++ .../src/providers/guest/provider.ts | 49 ++++++++ .../auth-backend/src/providers/providers.ts | 3 + yarn.lock | 34 +++++- 23 files changed, 620 insertions(+), 4 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/guest/index.ts create mode 100644 plugins/auth-backend-module-guest-provider/.eslintrc.js create mode 100644 plugins/auth-backend-module-guest-provider/README.md create mode 100644 plugins/auth-backend-module-guest-provider/package.json create mode 100644 plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts create mode 100644 plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts create mode 100644 plugins/auth-backend-module-guest-provider/src/index.ts create mode 100644 plugins/auth-backend-module-guest-provider/src/module.ts create mode 100644 plugins/auth-backend-module-guest-provider/src/resolvers.ts create mode 100644 plugins/auth-backend-module-guest-provider/src/types.ts create mode 100644 plugins/auth-backend/src/providers/guest/index.ts create mode 100644 plugins/auth-backend/src/providers/guest/provider.ts diff --git a/app-config.yaml b/app-config.yaml index 9b059da216..df0fd153f8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -399,6 +399,10 @@ auth: scopes: ${AUTH_ATLASSIAN_SCOPES} myproxy: development: {} + guest: + development: + clientId: t123 + clientSecret: test123 costInsights: engineerCost: 200000 engineerThreshold: 0.5 diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 4e9e1a492c..3285b05dcf 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -35,6 +35,7 @@ import { createFetchApi, FetchMiddlewares, VMwareCloudAuth, + GuestAuth, } from '@backstage/core-app-api'; import { @@ -58,6 +59,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + guestAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -277,6 +279,21 @@ export const apis = [ }); }, }), + + createApiFactory({ + api: guestAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => { + return GuestAuth.create({ + configApi, + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), createApiFactory({ api: permissionApiRef, deps: { diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 3d8bd45e5a..5357ad4d16 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -128,7 +128,7 @@ const app = createApp({ return ( diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 66f1460210..9f2ed58e8d 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -23,6 +23,7 @@ import { oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, + guestAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -74,4 +75,10 @@ export const providers = [ message: 'Sign In using Bitbucket Server', apiRef: bitbucketServerAuthApiRef, }, + { + id: 'guest-auth-provider', + title: 'Guest', + message: 'Sign in as a guest', + apiRef: guestAuthApiRef, + }, ]; diff --git a/packages/backend/package.json b/packages/backend/package.json index e6102b69cf..989d64eeec 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -35,6 +35,7 @@ "@backstage/plugin-adr-backend": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", + "@backstage/plugin-auth-backend-module-guest-provider": "^0.0.0", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", "@backstage/plugin-azure-sites-common": "workspace:^", diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 0d92315f92..773d3f4270 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -141,6 +141,8 @@ export default async function createPlugin( }, }, }), + + guest: providers.guest.create(), }, }); } diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts new file mode 100644 index 0000000000..88d4612973 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts @@ -0,0 +1,113 @@ +/* + * 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 { + AuthRequestOptions, + BackstageIdentityApi, + ProfileInfo, + ProfileInfoApi, + SessionApi, + SessionState, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; +import { DirectAuthConnector } from '../../../../lib/AuthConnector'; +import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { AuthApiCreateOptions } from '../types'; + +type GuestSession = { + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + +const DEFAULT_PROVIDER = { + id: 'guest', + title: 'Guest', + icon: () => null, +}; + +/** + * Implements a guest auth flow. + * + * @public + */ +export default class GuestAuth + implements ProfileInfoApi, BackstageIdentityApi, SessionApi +{ + static create(options: AuthApiCreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + } = options; + + const connector = new DirectAuthConnector({ + discoveryApi, + environment, + provider, + }); + + const sessionManager = new RefreshingAuthSessionManager({ + connector, + defaultScopes: new Set([]), + sessionScopes: (_: GuestSession) => new Set(), + sessionShouldRefresh: (session: GuestSession) => { + let min = Infinity; + if (session.backstageIdentity?.expiresAt) { + min = Math.min( + min, + (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000, + ); + } + return min < 60 * 5; + }, + }); + + return new GuestAuth({ sessionManager }); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + private readonly sessionManager: SessionManager; + + private constructor(options: { + sessionManager: SessionManager; + }) { + this.sessionManager = options.sessionManager; + } + + async signIn() { + await this.getBackstageIdentity({}); + } + async signOut() { + await this.sessionManager.removeSession(); + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/index.ts b/packages/core-app-api/src/apis/implementations/auth/guest/index.ts new file mode 100644 index 0000000000..42db58cfe6 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/guest/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { default as GuestAuth } from './GuestAuth'; 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..58db084760 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 './guest'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index d89544cf68..b11352b373 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -469,3 +469,15 @@ export const vmwareCloudAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.vmware-cloud', }); + +/** + * Provides guest authentication support. + * + * @public + * @remarks + */ +export const guestAuthApiRef: ApiRef< + ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.guest', +}); diff --git a/plugins/auth-backend-module-guest-provider/.eslintrc.js b/plugins/auth-backend-module-guest-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md new file mode 100644 index 0000000000..65da015958 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -0,0 +1,5 @@ +# backstage-plugin-auth-backend-module-guest-provider + +The guest-provider backend module for the auth plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json new file mode 100644 index 0000000000..c35162fd71 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -0,0 +1,42 @@ +{ + "name": "@backstage/plugin-auth-backend-module-guest-provider", + "description": "The guest-provider backend module for the auth plugin.", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "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/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "passport-oauth2": "^1.7.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "express": "^4.18.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts new file mode 100644 index 0000000000..8331870528 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts @@ -0,0 +1,54 @@ +/* + * 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 { SignInResolverFactory } from '@backstage/plugin-auth-node'; +import type { + AuthProviderFactory, + ProfileTransform, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { createGuestAuthRouteHandlers } from './createGuestAuthRouteHandlers'; +import { GuestInfo } from './types'; +import { guestResolver } from './resolvers'; + +/** @public */ +export function createGuestAuthProviderFactory(options?: { + profileTransform?: ProfileTransform; + signInResolver?: SignInResolver; + signInResolverFactories?: Record< + string, + SignInResolverFactory + >; +}): AuthProviderFactory { + return ctx => { + const signInResolver = options?.signInResolver ?? guestResolver(); + + if (!signInResolver) { + throw new Error( + `No sign-in resolver configured for guest auth provider '${ctx.providerId}'`, + ); + } + + return createGuestAuthRouteHandlers({ + signInResolver, + baseUrl: ctx.baseUrl, + appUrl: ctx.appUrl, + config: ctx.config, + resolverContext: ctx.resolverContext, + profileTransform: options?.profileTransform, + }); + }; +} diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts new file mode 100644 index 0000000000..07d0d3e87a --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Request, Response } from 'express'; +import type { Config } from '@backstage/config'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + ProfileTransform, + SignInResolver, + prepareBackstageIdentityResponse, + sendWebMessageResponse, +} from '@backstage/plugin-auth-node'; +import { GuestInfo } from './types'; + +/** @public */ +export interface GuestAuthRouteHandlersOptions { + config: Config; + baseUrl: string; + appUrl: string; + resolverContext: AuthResolverContext; + signInResolver: SignInResolver; + profileTransform?: ProfileTransform; +} + +const DEFAULT_RESULT: GuestInfo = { name: 'Guest' }; + +/** @public */ +export function createGuestAuthRouteHandlers( + options: GuestAuthRouteHandlersOptions, +): AuthProviderRouteHandlers { + const { resolverContext, signInResolver, appUrl } = options; + + const defaultTransform: ProfileTransform = async result => { + return { + profile: { + displayName: result.name, + }, + }; + }; + + const profileTransform = options.profileTransform ?? defaultTransform; + return { + async start(_, res): Promise { + res.redirect('handler/frame'); + }, + + async frameHandler(_, res): Promise { + const { profile } = await profileTransform( + DEFAULT_RESULT, + resolverContext, + ); + const response: ClientAuthResponse = { + profile, + providerInfo: { + name: 'Guest', + }, + }; + if (signInResolver) { + const identity = await signInResolver( + { profile, result: DEFAULT_RESULT }, + resolverContext, + ); + response.backstageIdentity = prepareBackstageIdentityResponse(identity); + } + // post message back to popup if successful + sendWebMessageResponse(res, appUrl, { + type: 'authorization_response', + response, + }); + }, + + async refresh(this: never, _: Request, res: Response): Promise { + const { profile } = await profileTransform( + DEFAULT_RESULT, + resolverContext, + ); + + const identity = await signInResolver( + { profile, result: DEFAULT_RESULT }, + resolverContext, + ); + + const response: ClientAuthResponse<{}> = { + profile, + providerInfo: {}, + backstageIdentity: prepareBackstageIdentityResponse(identity), + }; + + res.status(200).json(response); + }, + + async logout(_, res) { + res.end(); + }, + }; +} diff --git a/plugins/auth-backend-module-guest-provider/src/index.ts b/plugins/auth-backend-module-guest-provider/src/index.ts new file mode 100644 index 0000000000..b1a89763b9 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The guest-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { createGuestAuthProviderFactory } from './createGuestAuthFactory'; +export type { GuestInfo } from './types'; +export { authModuleGuestProvider as default } from './module'; diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts new file mode 100644 index 0000000000..c9fd3feea4 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -0,0 +1,44 @@ +/* + * 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { + createOAuthProviderFactory, + commonSignInResolvers, + authProvidersExtensionPoint, +} from '@backstage/plugin-auth-node'; +import { createGuestAuthProviderFactory } from './createGuestAuthFactory'; + +export const authModuleGuestProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'guest-provider', + register(reg) { + reg.registerInit({ + deps: { + logger: coreServices.logger, + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'guest', + factory: createGuestAuthProviderFactory(), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts new file mode 100644 index 0000000000..47d340f013 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -0,0 +1,40 @@ +/* + * 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 { stringifyEntityRef } from '@backstage/catalog-model'; +import { createSignInResolverFactory } from '@backstage/plugin-auth-node'; + +export const guestResolver = createSignInResolverFactory({ + create() { + return async (_, ctx) => { + const userRef = stringifyEntityRef({ + kind: 'user', + name: 'guest', + }); + try { + return ctx.signInWithCatalogUser({ entityRef: userRef }); + } catch (err) { + // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, + return ctx.issueToken({ + claims: { + sub: userRef, + ent: [userRef], + }, + }); + } + }; + }, +}); diff --git a/plugins/auth-backend-module-guest-provider/src/types.ts b/plugins/auth-backend-module-guest-provider/src/types.ts new file mode 100644 index 0000000000..9d0ace0a33 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/src/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { ProfileTransform } from '@backstage/plugin-auth-node'; + +export type GuestInfo = { + name: string; +}; + +export interface GuestAuthenticator { + defaultProfileTransform: ProfileTransform; +} diff --git a/plugins/auth-backend/src/providers/guest/index.ts b/plugins/auth-backend/src/providers/guest/index.ts new file mode 100644 index 0000000000..7b384798b0 --- /dev/null +++ b/plugins/auth-backend/src/providers/guest/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { guest } from './provider'; diff --git a/plugins/auth-backend/src/providers/guest/provider.ts b/plugins/auth-backend/src/providers/guest/provider.ts new file mode 100644 index 0000000000..7d3a9e724c --- /dev/null +++ b/plugins/auth-backend/src/providers/guest/provider.ts @@ -0,0 +1,49 @@ +/* + * 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 { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthHandler, SignInResolver } from '../types'; +import { createGuestAuthProviderFactory } from '@backstage/plugin-auth-backend-module-guest-provider'; +import { GuestInfo } from '@backstage/plugin-auth-backend-module-guest-provider'; + +/** + * Auth provider integration for Google auth + * + * @public + */ +export const guest = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return createGuestAuthProviderFactory({ + profileTransform: options?.authHandler, + signInResolver: options?.signIn?.resolver, + }); + }, +}); diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index 76ac51f662..d527bf8b13 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -30,6 +30,7 @@ import { oidc } from './oidc'; import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; +import { guest } from './guest'; import { bitbucketServer } from './bitbucketServer'; import { easyAuth } from './azure-easyauth'; import { AuthProviderFactory } from '@backstage/plugin-auth-node'; @@ -58,6 +59,7 @@ export const providers = Object.freeze({ onelogin, saml, easyAuth, + guest, }); /** @@ -83,4 +85,5 @@ export const defaultAuthProviderFactories: { bitbucket: bitbucket.create(), bitbucketServer: bitbucketServer.create(), atlassian: atlassian.create(), + guest: guest.create(), }; diff --git a/yarn.lock b/yarn.lock index 73413df542..d1af60c782 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,3 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - __metadata: version: 6 cacheKey: 8 @@ -4682,6 +4679,22 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-guest-provider@^0.0.0, @backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + express: ^4.18.2 + passport-oauth2: ^1.7.0 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:^, @backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-microsoft-provider@workspace:plugins/auth-backend-module-microsoft-provider" @@ -4838,6 +4851,7 @@ __metadata: "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": ^0.0.0 "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^" @@ -27446,6 +27460,7 @@ __metadata: "@backstage/plugin-adr-backend": "workspace:^" "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": ^0.0.0 "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-backend": "workspace:^" "@backstage/plugin-azure-sites-common": "workspace:^" @@ -37346,6 +37361,19 @@ __metadata: languageName: node linkType: hard +"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": + version: 1.7.0 + resolution: "passport-oauth2@npm:1.7.0" + dependencies: + base64url: 3.x.x + oauth: 0.10.x + passport-strategy: 1.x.x + uid2: 0.0.x + utils-merge: 1.x.x + checksum: a9a80b968343c9c1906f74ef613b346ec2d6a6acfe17af81e673fd774779b436729252485755c3ce182f2cdba2434d75067418952d722404d65b93c0360ca02b + languageName: node + linkType: hard + "passport-oauth@npm:1.0.0, passport-oauth@npm:^1.0.0": version: 1.0.0 resolution: "passport-oauth@npm:1.0.0" From 08b7c8a59434b0a3502dd70cd0d2dc7a9158d5ec Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:08:35 -0500 Subject: [PATCH 346/483] needed to support refreshing the token Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .../implementations/auth/guest/GuestAuth.ts | 4 +- .../lib/AuthConnector/DirectAuthConnector.ts | 2 +- .../RefreshingDirectAuthConnector.ts | 54 +++++++++++++++++++ .../src/createGuestAuthRouteHandlers.ts | 7 ++- .../src/module.ts | 6 +-- 5 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts index 88d4612973..880aa0f675 100644 --- a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts @@ -24,10 +24,10 @@ import { BackstageIdentityResponse, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; -import { DirectAuthConnector } from '../../../../lib/AuthConnector'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthApiCreateOptions } from '../types'; +import { RefreshingDirectAuthConnector } from '../../../../lib/AuthConnector/RefreshingDirectAuthConnector'; type GuestSession = { profile: ProfileInfo; @@ -55,7 +55,7 @@ export default class GuestAuth provider = DEFAULT_PROVIDER, } = options; - const connector = new DirectAuthConnector({ + const connector = new RefreshingDirectAuthConnector({ discoveryApi, environment, provider, diff --git a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts index 200ba755ac..4cb0553efc 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -70,7 +70,7 @@ export class DirectAuthConnector { } } - private async buildUrl(path: string): Promise { + protected async buildUrl(path: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('auth'); return `${baseUrl}/${this.provider.id}${path}?env=${this.environment}`; } diff --git a/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts new file mode 100644 index 0000000000..34969bf356 --- /dev/null +++ b/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts @@ -0,0 +1,54 @@ +/* + * 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 { DirectAuthConnector } from './DirectAuthConnector'; + +export class RefreshingDirectAuthConnector< + DirectAuthResponse, +> extends DirectAuthConnector { + async refreshSession(): Promise { + const res = await fetch( + `${await this.buildUrl('/refresh')}&optional=true`, + { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + credentials: 'include', + }, + ).catch(error => { + throw new Error(`Auth refresh request failed, ${error}`); + }); + + if (!res.ok) { + const error: any = new Error( + `Auth refresh request failed, ${res.statusText}`, + ); + error.status = res.status; + throw error; + } + + const authInfo = await res.json(); + + if (authInfo.error) { + const error = new Error(authInfo.error.message); + if (authInfo.error.name) { + error.name = authInfo.error.name; + } + throw error; + } + return authInfo; + } +} diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts index 07d0d3e87a..8d1cfe78b4 100644 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts @@ -56,6 +56,7 @@ export function createGuestAuthRouteHandlers( const profileTransform = options.profileTransform ?? defaultTransform; return { async start(_, res): Promise { + // We are the auth provider for guests, skip this step. res.redirect('handler/frame'); }, @@ -66,9 +67,7 @@ export function createGuestAuthRouteHandlers( ); const response: ClientAuthResponse = { profile, - providerInfo: { - name: 'Guest', - }, + providerInfo: DEFAULT_RESULT, }; if (signInResolver) { const identity = await signInResolver( @@ -97,7 +96,7 @@ export function createGuestAuthRouteHandlers( const response: ClientAuthResponse<{}> = { profile, - providerInfo: {}, + providerInfo: DEFAULT_RESULT, backstageIdentity: prepareBackstageIdentityResponse(identity), }; diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index c9fd3feea4..69b5eba48d 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -17,11 +17,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { - createOAuthProviderFactory, - commonSignInResolvers, - authProvidersExtensionPoint, -} from '@backstage/plugin-auth-node'; +import { authProvidersExtensionPoint } from '@backstage/plugin-auth-node'; import { createGuestAuthProviderFactory } from './createGuestAuthFactory'; export const authModuleGuestProvider = createBackendModule({ From 1bedb23da027f2ad5b9e29fcfcb8ab84e9f6d47e Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:10:36 -0500 Subject: [PATCH 347/483] add changesets Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .changeset/cold-boats-sell.md | 5 +++++ .changeset/gentle-starfishes-camp.md | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/cold-boats-sell.md create mode 100644 .changeset/gentle-starfishes-camp.md diff --git a/.changeset/cold-boats-sell.md b/.changeset/cold-boats-sell.md new file mode 100644 index 0000000000..112d9022bc --- /dev/null +++ b/.changeset/cold-boats-sell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-guest-provider': patch +--- + +Adds a new guest provider that maps guest users to actual tokens. diff --git a/.changeset/gentle-starfishes-camp.md b/.changeset/gentle-starfishes-camp.md new file mode 100644 index 0000000000..229d4c39c9 --- /dev/null +++ b/.changeset/gentle-starfishes-camp.md @@ -0,0 +1,8 @@ +--- +'@backstage/core-plugin-api': minor +'@backstage/app-defaults': minor +'@backstage/core-app-api': minor +'@backstage/plugin-auth-backend': minor +--- + +Adds in support for the new guest provider added by `@backstage/plugin-auth-backend-module-guest-provider`. From 1aedf6c5245f12f80c1f070870da83447605b4d1 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:11:37 -0500 Subject: [PATCH 348/483] update app config to remove unnecessary keys Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- app-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index df0fd153f8..57d4941f2f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -401,8 +401,7 @@ auth: development: {} guest: development: - clientId: t123 - clientSecret: test123 + costInsights: engineerCost: 200000 engineerThreshold: 0.5 From 085ddf4084f67d008d7c9d05d4bd597e8c7d389f Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:31:08 -0500 Subject: [PATCH 349/483] add comments Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- packages/app/src/identityProviders.ts | 12 ++-- .../implementations/auth/guest/GuestAuth.ts | 2 +- .../RefreshingDirectAuthConnector.ts | 6 ++ .../src/createGuestAuthFactory.ts | 21 +++--- .../src/createGuestAuthRouteHandlers.ts | 71 +++++++------------ .../src/index.ts | 1 - .../src/resolvers.ts | 6 ++ .../src/types.ts | 6 +- 8 files changed, 60 insertions(+), 65 deletions(-) diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 9f2ed58e8d..c59a55b9d1 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -27,6 +27,12 @@ import { } from '@backstage/core-plugin-api'; export const providers = [ + { + id: 'guest-auth-provider', + title: 'Guest', + message: 'Sign in as a guest', + apiRef: guestAuthApiRef, + }, { id: 'google-auth-provider', title: 'Google', @@ -75,10 +81,4 @@ export const providers = [ message: 'Sign In using Bitbucket Server', apiRef: bitbucketServerAuthApiRef, }, - { - id: 'guest-auth-provider', - title: 'Guest', - message: 'Sign in as a guest', - apiRef: guestAuthApiRef, - }, ]; diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts index 880aa0f675..b054187373 100644 --- a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts @@ -41,7 +41,7 @@ const DEFAULT_PROVIDER = { }; /** - * Implements a guest auth flow. + * Implements a guest auth flow. Heavily based on SAML flow with added support for refreshing the token. * * @public */ diff --git a/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts index 34969bf356..94f7486210 100644 --- a/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts @@ -16,9 +16,15 @@ import { DirectAuthConnector } from './DirectAuthConnector'; +/** + * Add support for refreshing direct tokens. Used for guest authentication. + */ export class RefreshingDirectAuthConnector< DirectAuthResponse, > extends DirectAuthConnector { + /** + * Pulled from DefaultAuthConnector and adapted for use with DirectAuthConnector. + */ async refreshSession(): Promise { const res = await fetch( `${await this.buildUrl('/refresh')}&optional=true`, diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts index 8331870528..acd08940a3 100644 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts @@ -21,17 +21,21 @@ import type { SignInResolver, } from '@backstage/plugin-auth-node'; import { createGuestAuthRouteHandlers } from './createGuestAuthRouteHandlers'; -import { GuestInfo } from './types'; import { guestResolver } from './resolvers'; +const defaultTransform: ProfileTransform<{}> = async () => { + return { + profile: { + displayName: 'Guest', + }, + }; +}; + /** @public */ export function createGuestAuthProviderFactory(options?: { - profileTransform?: ProfileTransform; - signInResolver?: SignInResolver; - signInResolverFactories?: Record< - string, - SignInResolverFactory - >; + profileTransform?: ProfileTransform<{}>; + signInResolver?: SignInResolver<{}>; + signInResolverFactories?: Record>; }): AuthProviderFactory { return ctx => { const signInResolver = options?.signInResolver ?? guestResolver(); @@ -41,6 +45,7 @@ export function createGuestAuthProviderFactory(options?: { `No sign-in resolver configured for guest auth provider '${ctx.providerId}'`, ); } + const profileTransform = options?.profileTransform ?? defaultTransform; return createGuestAuthRouteHandlers({ signInResolver, @@ -48,7 +53,7 @@ export function createGuestAuthProviderFactory(options?: { appUrl: ctx.appUrl, config: ctx.config, resolverContext: ctx.resolverContext, - profileTransform: options?.profileTransform, + profileTransform, }); }; } diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts index 8d1cfe78b4..ab4f9922cd 100644 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts @@ -25,7 +25,6 @@ import { prepareBackstageIdentityResponse, sendWebMessageResponse, } from '@backstage/plugin-auth-node'; -import { GuestInfo } from './types'; /** @public */ export interface GuestAuthRouteHandlersOptions { @@ -33,77 +32,61 @@ export interface GuestAuthRouteHandlersOptions { baseUrl: string; appUrl: string; resolverContext: AuthResolverContext; - signInResolver: SignInResolver; - profileTransform?: ProfileTransform; + signInResolver: SignInResolver<{}>; + profileTransform: ProfileTransform<{}>; } -const DEFAULT_RESULT: GuestInfo = { name: 'Guest' }; - /** @public */ export function createGuestAuthRouteHandlers( options: GuestAuthRouteHandlersOptions, ): AuthProviderRouteHandlers { - const { resolverContext, signInResolver, appUrl } = options; + const { resolverContext, signInResolver, appUrl, profileTransform } = options; + + const createGuestSession = async (): Promise> => { + const { profile } = await profileTransform({}, resolverContext); + + const identity = await signInResolver( + { profile, result: {} }, + resolverContext, + ); - const defaultTransform: ProfileTransform = async result => { return { - profile: { - displayName: result.name, - }, + profile, + providerInfo: {}, + backstageIdentity: prepareBackstageIdentityResponse(identity), }; }; - const profileTransform = options.profileTransform ?? defaultTransform; return { async start(_, res): Promise { // We are the auth provider for guests, skip this step. res.redirect('handler/frame'); }, + /** + * This is where we create the token for the guest user. You can override the + * entityRef for the guest user with `signInResolver`. + */ async frameHandler(_, res): Promise { - const { profile } = await profileTransform( - DEFAULT_RESULT, - resolverContext, - ); - const response: ClientAuthResponse = { - profile, - providerInfo: DEFAULT_RESULT, - }; - if (signInResolver) { - const identity = await signInResolver( - { profile, result: DEFAULT_RESULT }, - resolverContext, - ); - response.backstageIdentity = prepareBackstageIdentityResponse(identity); - } + const session = await createGuestSession(); // post message back to popup if successful sendWebMessageResponse(res, appUrl, { type: 'authorization_response', - response, + response: session, }); }, + /** + * Support refreshing the guest user's token. This should just improve the experience of + * browsing while in guest mode. + */ async refresh(this: never, _: Request, res: Response): Promise { - const { profile } = await profileTransform( - DEFAULT_RESULT, - resolverContext, - ); - - const identity = await signInResolver( - { profile, result: DEFAULT_RESULT }, - resolverContext, - ); - - const response: ClientAuthResponse<{}> = { - profile, - providerInfo: DEFAULT_RESULT, - backstageIdentity: prepareBackstageIdentityResponse(identity), - }; - - res.status(200).json(response); + const session = await createGuestSession(); + res.status(200).json(session); }, async logout(_, res) { + // If we don't send a response or it gets cached into a 204, the page will hang. res.end(); }, }; diff --git a/plugins/auth-backend-module-guest-provider/src/index.ts b/plugins/auth-backend-module-guest-provider/src/index.ts index b1a89763b9..0c4a382a87 100644 --- a/plugins/auth-backend-module-guest-provider/src/index.ts +++ b/plugins/auth-backend-module-guest-provider/src/index.ts @@ -21,5 +21,4 @@ */ export { createGuestAuthProviderFactory } from './createGuestAuthFactory'; -export type { GuestInfo } from './types'; export { authModuleGuestProvider as default } from './module'; diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts index 47d340f013..5922530c5c 100644 --- a/plugins/auth-backend-module-guest-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -17,6 +17,12 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { createSignInResolverFactory } from '@backstage/plugin-auth-node'; +/** + * Provide a default implementation of the user to resolve to. By default, this + * is `user:default/guest`. We will attempt to get that user if they're in the + * catalog. If that user doesn't exist in the catalog, we will still create a + * token for them so they can keep viewing. + */ export const guestResolver = createSignInResolverFactory({ create() { return async (_, ctx) => { diff --git a/plugins/auth-backend-module-guest-provider/src/types.ts b/plugins/auth-backend-module-guest-provider/src/types.ts index 9d0ace0a33..c831014cb5 100644 --- a/plugins/auth-backend-module-guest-provider/src/types.ts +++ b/plugins/auth-backend-module-guest-provider/src/types.ts @@ -16,10 +16,6 @@ import { ProfileTransform } from '@backstage/plugin-auth-node'; -export type GuestInfo = { - name: string; -}; - export interface GuestAuthenticator { - defaultProfileTransform: ProfileTransform; + defaultProfileTransform: ProfileTransform<{}>; } From bb710815b2fd76562c6fa1cdc225fff7f6ff7811 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:53:43 -0500 Subject: [PATCH 350/483] adding more documentation Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .../src/layout/SignInPage/providers.tsx | 1 + .../README.md | 78 ++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index e456a6948b..20613b7509 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -43,6 +43,7 @@ export type SignInProviderType = { }; const signInProviders: { [key: string]: SignInProvider } = { + /** @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` */ guest: guestProvider, custom: customProvider, common: commonProvider, diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md index 65da015958..9c297aebe6 100644 --- a/plugins/auth-backend-module-guest-provider/README.md +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -1,5 +1,77 @@ -# backstage-plugin-auth-backend-module-guest-provider +# Auth Module: Guest Provider -The guest-provider backend module for the auth plugin. +This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state. -_This plugin was created through the Backstage CLI_ +**NOTE**: + +## Installation + +### Backend + +#### New Backend + +```diff +const backend = createBackend(); +... + ++backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); + +... +backend.start(); +``` + +#### Old Backend + +This module was also backported for the old backend and can be used like so, + +```diff ++import { ++ providers, ++} from '@backstage/plugin-auth-backend'; + .... + return await createRouter({ + ... + providerFactories: { + gitlab: providers.gitlab(), ++ guest: providers.guest(), + ... + } + ... +``` + +### Frontend + +Add the following to your `SignInPage` providers, + +```diff ++import { ++ guestAuthApiRef, ++} from '@backstage/core-plugin-api'; + +const providers = [ ++ { ++ id: 'guest-auth-provider', ++ title: 'Guest', ++ message: 'Sign in as a guest', ++ apiRef: guestAuthApiRef, ++ }, + ... +``` + +### Config + +Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`, + +```diff +auth: + providers: ++ guest: ++ development: {} +``` + +We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-guest-provider) From d1be48bcfabb3df44006afdf75636dbe31da68e8 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 16:59:44 -0500 Subject: [PATCH 351/483] add warning and prevent startup in production. Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/auth-backend-module-guest-provider/README.md | 2 +- plugins/auth-backend-module-guest-provider/src/module.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md index 9c297aebe6..471b522859 100644 --- a/plugins/auth-backend-module-guest-provider/README.md +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -2,7 +2,7 @@ This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state. -**NOTE**: +**NOTE**: This provider should only ever be enabled for `development` or `test`. Enabling this for production is strongly discouraged as it would give everyone a way to bypass your other authentication methods. ## Installation diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index 69b5eba48d..ad5c8c95a0 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -30,6 +30,11 @@ export const authModuleGuestProvider = createBackendModule({ providers: authProvidersExtensionPoint, }, async init({ providers }) { + if (process.env.NODE_ENV === 'production') { + throw new Error( + 'Guest provider does not support authenticating production workloads.', + ); + } providers.registerProvider({ providerId: 'guest', factory: createGuestAuthProviderFactory(), From a83eb21b89bc05135329fa07493d48d821a7483b Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:02:11 -0500 Subject: [PATCH 352/483] add object instead of empty Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- app-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-config.yaml b/app-config.yaml index 57d4941f2f..e18c45aefa 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -400,7 +400,7 @@ auth: myproxy: development: {} guest: - development: + development: {} costInsights: engineerCost: 200000 From a8f7904588980b8a0f13e9e9cedf191ecdb36585 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:07:59 -0500 Subject: [PATCH 353/483] fix build issues Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .changeset/selfish-glasses-cheer.md | 5 +++++ packages/backend/package.json | 1 - plugins/auth-backend-module-guest-provider/package.json | 1 - yarn.lock | 4 +--- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 .changeset/selfish-glasses-cheer.md diff --git a/.changeset/selfish-glasses-cheer.md b/.changeset/selfish-glasses-cheer.md new file mode 100644 index 0000000000..0a56a8b489 --- /dev/null +++ b/.changeset/selfish-glasses-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +**DEPRECATED** `SignInPage`'s `'guest'` provider is deprecated. Use `@backstage/plugin-auth-backend-module-guest-provider` instead. diff --git a/packages/backend/package.json b/packages/backend/package.json index 989d64eeec..e6102b69cf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -35,7 +35,6 @@ "@backstage/plugin-adr-backend": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", - "@backstage/plugin-auth-backend-module-guest-provider": "^0.0.0", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", "@backstage/plugin-azure-sites-common": "workspace:^", diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index c35162fd71..ae50007225 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -5,7 +5,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", diff --git a/yarn.lock b/yarn.lock index d1af60c782..f2f121736c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4679,7 +4679,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-auth-backend-module-guest-provider@^0.0.0, @backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": +"@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider" dependencies: @@ -4851,7 +4851,6 @@ __metadata: "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" - "@backstage/plugin-auth-backend-module-guest-provider": ^0.0.0 "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^" @@ -27460,7 +27459,6 @@ __metadata: "@backstage/plugin-adr-backend": "workspace:^" "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" - "@backstage/plugin-auth-backend-module-guest-provider": ^0.0.0 "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-backend": "workspace:^" "@backstage/plugin-azure-sites-common": "workspace:^" From 8d8e37abcc57c82746c5c22b24da5ba65a0a3b2f Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:12:58 -0500 Subject: [PATCH 354/483] fix tsc issue Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/auth-backend/src/providers/guest/provider.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/src/providers/guest/provider.ts b/plugins/auth-backend/src/providers/guest/provider.ts index 7d3a9e724c..2efc584d5d 100644 --- a/plugins/auth-backend/src/providers/guest/provider.ts +++ b/plugins/auth-backend/src/providers/guest/provider.ts @@ -16,7 +16,6 @@ import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { AuthHandler, SignInResolver } from '../types'; import { createGuestAuthProviderFactory } from '@backstage/plugin-auth-backend-module-guest-provider'; -import { GuestInfo } from '@backstage/plugin-auth-backend-module-guest-provider'; /** * Auth provider integration for Google auth @@ -29,7 +28,7 @@ export const guest = createAuthProviderIntegration({ * The profile transformation function used to verify and convert the auth response * into the profile that will be presented to the user. */ - authHandler?: AuthHandler; + authHandler?: AuthHandler<{}>; /** * Configure sign-in for this provider, without it the provider can not be used to sign users in. @@ -38,7 +37,7 @@ export const guest = createAuthProviderIntegration({ /** * Maps an auth result to a Backstage identity for the user. */ - resolver: SignInResolver; + resolver: SignInResolver<{}>; }; }) { return createGuestAuthProviderFactory({ From 4506a1b2241c390d7e18e6338d09b9401f263cce Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:13:38 -0500 Subject: [PATCH 355/483] add dependency Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/auth-backend/package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 456cc18af2..3ea1d26475 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -49,6 +49,7 @@ "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^", diff --git a/yarn.lock b/yarn.lock index f2f121736c..769d1f52a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4679,7 +4679,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": +"@backstage/plugin-auth-backend-module-guest-provider@workspace:^, @backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-guest-provider@workspace:plugins/auth-backend-module-guest-provider" dependencies: @@ -4851,6 +4851,7 @@ __metadata: "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^" From d4b0688c6d9f1fdf11448803f600452f51b9d808 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:33:34 -0500 Subject: [PATCH 356/483] fix test case Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- packages/frontend-app-api/src/wiring/createApp.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 70f8cfa2ae..af9bfaf751 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -292,6 +292,7 @@ describe('createApp', () => { + ] " From 68c6f67f0cacd0b138cc7a80d37aaf5387d33a3b Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 17:38:13 -0500 Subject: [PATCH 357/483] fix visibility tags Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .../api-report.md | 22 +++++++++++++++++++ .../src/createGuestAuthRouteHandlers.ts | 2 -- .../src/module.ts | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 plugins/auth-backend-module-guest-provider/api-report.md diff --git a/plugins/auth-backend-module-guest-provider/api-report.md b/plugins/auth-backend-module-guest-provider/api-report.md new file mode 100644 index 0000000000..adaf4313fb --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-auth-backend-module-guest-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import type { AuthProviderFactory } from '@backstage/plugin-auth-node'; +import { BackendFeature } from '@backstage/backend-plugin-api'; +import type { ProfileTransform } from '@backstage/plugin-auth-node'; +import type { SignInResolver } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleGuestProvider: () => BackendFeature; +export default authModuleGuestProvider; + +// @public (undocumented) +export function createGuestAuthProviderFactory(options?: { + profileTransform?: ProfileTransform<{}>; + signInResolver?: SignInResolver<{}>; + signInResolverFactories?: Record>; +}): AuthProviderFactory; +``` diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts index ab4f9922cd..76a69ca75b 100644 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts +++ b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts @@ -26,7 +26,6 @@ import { sendWebMessageResponse, } from '@backstage/plugin-auth-node'; -/** @public */ export interface GuestAuthRouteHandlersOptions { config: Config; baseUrl: string; @@ -36,7 +35,6 @@ export interface GuestAuthRouteHandlersOptions { profileTransform: ProfileTransform<{}>; } -/** @public */ export function createGuestAuthRouteHandlers( options: GuestAuthRouteHandlersOptions, ): AuthProviderRouteHandlers { diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index ad5c8c95a0..4d191ac9e1 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -20,6 +20,7 @@ import { import { authProvidersExtensionPoint } from '@backstage/plugin-auth-node'; import { createGuestAuthProviderFactory } from './createGuestAuthFactory'; +/** @public */ export const authModuleGuestProvider = createBackendModule({ pluginId: 'auth', moduleId: 'guest-provider', From 215a37bc3795ba963eacff41c968403225461886 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 18:06:29 -0500 Subject: [PATCH 358/483] fix api reports again Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- packages/core-app-api/api-report.md | 20 ++++++++++++++++++++ packages/core-plugin-api/api-report.md | 5 +++++ plugins/auth-backend/api-report.md | 15 +++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 8b8cc991c8..0ecb0193db 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -464,6 +464,26 @@ export class GoogleAuth { static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } +// @public +export class GuestAuth + implements ProfileInfoApi, BackstageIdentityApi, SessionApi +{ + // (undocumented) + static create(options: AuthApiCreateOptions): GuestAuth; + // (undocumented) + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; + // (undocumented) + getProfile(options?: AuthRequestOptions): Promise; + // (undocumented) + sessionState$(): Observable; + // (undocumented) + signIn(): Promise; + // (undocumented) + signOut(): Promise; +} + // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 2a993687b3..ad91a0431e 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -503,6 +503,11 @@ export const googleAuthApiRef: ApiRef< SessionApi >; +// @public +export const guestAuthApiRef: ApiRef< + ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public export type IconComponent = ComponentType< | { diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 1beddb6419..431caf3e91 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -644,6 +644,21 @@ export const providers: Readonly<{ ) => AuthProviderFactory_2; resolvers: never; }>; + guest: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler<{}> | undefined; + signIn?: + | { + resolver: SignInResolver<{}>; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory_2; + resolvers: never; + }>; }>; // @public @deprecated (undocumented) From 875d1137c771421a83ad86fba0f6a8613fc97590 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sat, 27 Jan 2024 21:25:44 -0500 Subject: [PATCH 359/483] add catalog-info file Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .../catalog-info.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 plugins/auth-backend-module-guest-provider/catalog-info.yaml diff --git a/plugins/auth-backend-module-guest-provider/catalog-info.yaml b/plugins/auth-backend-module-guest-provider/catalog-info.yaml new file mode 100644 index 0000000000..5d3513296b --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-guest-provider + title: '@backstage/plugin-auth-backend-module-guest-provider' + description: The guest-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers From 1c64b2af45d88056655d169027534dc72874bfb6 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sun, 11 Feb 2024 12:45:38 -0500 Subject: [PATCH 360/483] update to a proxied sign in identity instead of a separate guest provider Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- app-config.yaml | 5 +- packages/app-defaults/src/defaults/apis.ts | 17 --- packages/app/src/App.tsx | 2 +- packages/app/src/identityProviders.ts | 7 -- packages/backend-next/package.json | 1 + packages/backend-next/src/index.ts | 3 + .../implementations/auth/guest/GuestAuth.ts | 113 ------------------ .../apis/implementations/auth/guest/index.ts | 16 --- .../src/apis/implementations/auth/index.ts | 1 - .../lib/AuthConnector/DirectAuthConnector.ts | 2 +- .../layout/SignInPage/GuestUserIdentity.ts | 3 + .../src/layout/SignInPage/guestProvider.tsx | 78 +++++++----- .../src/layout/SignInPage/providers.tsx | 1 - .../src/apis/definitions/auth.ts | 12 -- .../src/wiring/createApp.test.tsx | 1 - .../README.md | 33 +---- .../src/authenticator.ts} | 12 +- .../src/createGuestAuthFactory.ts | 59 --------- .../src/createGuestAuthRouteHandlers.ts | 91 -------------- .../src/index.ts | 1 - .../src/module.ts | 15 ++- .../src/resolvers.ts | 40 +++---- .../src/providers/guest/provider.ts | 48 -------- .../auth-backend/src/providers/providers.ts | 3 - yarn.lock | 19 +-- 25 files changed, 108 insertions(+), 475 deletions(-) delete mode 100644 packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts delete mode 100644 packages/core-app-api/src/apis/implementations/auth/guest/index.ts rename plugins/{auth-backend/src/providers/guest/index.ts => auth-backend-module-guest-provider/src/authenticator.ts} (67%) delete mode 100644 plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts delete mode 100644 plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts delete mode 100644 plugins/auth-backend/src/providers/guest/provider.ts diff --git a/app-config.yaml b/app-config.yaml index e18c45aefa..b864c3b610 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -400,7 +400,10 @@ auth: myproxy: development: {} guest: - development: {} + development: + signIn: + resolvers: + - resolver: guestUser costInsights: engineerCost: 200000 diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 3285b05dcf..4e9e1a492c 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -35,7 +35,6 @@ import { createFetchApi, FetchMiddlewares, VMwareCloudAuth, - GuestAuth, } from '@backstage/core-app-api'; import { @@ -59,7 +58,6 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, - guestAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -279,21 +277,6 @@ export const apis = [ }); }, }), - - createApiFactory({ - api: guestAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => { - return GuestAuth.create({ - configApi, - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }); - }, - }), createApiFactory({ api: permissionApiRef, deps: { diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 5357ad4d16..3d8bd45e5a 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -128,7 +128,7 @@ const app = createApp({ return ( diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index c59a55b9d1..66f1460210 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -23,16 +23,9 @@ import { oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, - guestAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ - { - id: 'guest-auth-provider', - title: 'Guest', - message: 'Sign in as a guest', - apiRef: guestAuthApiRef, - }, { id: 'google-auth-provider', title: 'Google', diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 74016539b0..333484051b 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -33,6 +33,7 @@ "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", "@backstage/plugin-badges-backend": "workspace:^", diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index e4dd127208..58fa994658 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -57,4 +57,7 @@ backend.add(import('@backstage/plugin-sonarqube-backend')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); + backend.start(); diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts b/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts deleted file mode 100644 index b054187373..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/guest/GuestAuth.ts +++ /dev/null @@ -1,113 +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 { - AuthRequestOptions, - BackstageIdentityApi, - ProfileInfo, - ProfileInfoApi, - SessionApi, - SessionState, - BackstageIdentityResponse, -} from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; -import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { AuthApiCreateOptions } from '../types'; -import { RefreshingDirectAuthConnector } from '../../../../lib/AuthConnector/RefreshingDirectAuthConnector'; - -type GuestSession = { - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - -const DEFAULT_PROVIDER = { - id: 'guest', - title: 'Guest', - icon: () => null, -}; - -/** - * Implements a guest auth flow. Heavily based on SAML flow with added support for refreshing the token. - * - * @public - */ -export default class GuestAuth - implements ProfileInfoApi, BackstageIdentityApi, SessionApi -{ - static create(options: AuthApiCreateOptions) { - const { - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - } = options; - - const connector = new RefreshingDirectAuthConnector({ - discoveryApi, - environment, - provider, - }); - - const sessionManager = new RefreshingAuthSessionManager({ - connector, - defaultScopes: new Set([]), - sessionScopes: (_: GuestSession) => new Set(), - sessionShouldRefresh: (session: GuestSession) => { - let min = Infinity; - if (session.backstageIdentity?.expiresAt) { - min = Math.min( - min, - (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000, - ); - } - return min < 60 * 5; - }, - }); - - return new GuestAuth({ sessionManager }); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - private readonly sessionManager: SessionManager; - - private constructor(options: { - sessionManager: SessionManager; - }) { - this.sessionManager = options.sessionManager; - } - - async signIn() { - await this.getBackstageIdentity({}); - } - async signOut() { - await this.sessionManager.removeSession(); - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } -} diff --git a/packages/core-app-api/src/apis/implementations/auth/guest/index.ts b/packages/core-app-api/src/apis/implementations/auth/guest/index.ts deleted file mode 100644 index 42db58cfe6..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/guest/index.ts +++ /dev/null @@ -1,16 +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. - */ -export { default as GuestAuth } from './GuestAuth'; 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 58db084760..e02e07961a 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -26,5 +26,4 @@ export * from './bitbucket'; export * from './bitbucketServer'; export * from './atlassian'; export * from './vmwareCloud'; -export * from './guest'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts index 4cb0553efc..200ba755ac 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -70,7 +70,7 @@ export class DirectAuthConnector { } } - protected async buildUrl(path: string): Promise { + private async buildUrl(path: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('auth'); return `${baseUrl}/${this.provider.id}${path}?env=${this.environment}`; } diff --git a/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts index db4731704c..6abb412090 100644 --- a/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts @@ -20,6 +20,9 @@ import { BackstageUserIdentity, } from '@backstage/core-plugin-api'; +/** + * @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` instead. + */ export class GuestUserIdentity implements IdentityApi { getUserId(): string { return 'guest'; diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 5393c1ea89..018feb2241 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,39 +20,55 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { GuestUserIdentity } from './GuestUserIdentity'; +import { ProxiedSignInIdentity } from '../ProxiedSignInPage/ProxiedSignInIdentity'; +import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; -const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => ( - - { - onSignInStarted(); - onSignInSuccess(new GuestUserIdentity()); - }} - > - Enter - - } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- meaning some features might be unavailable. -
-
-
-); +const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { + const discoveryApi = useApi(discoveryApiRef); + return ( + + { + onSignInStarted(); + onSignInSuccess( + new ProxiedSignInIdentity({ + provider: 'guest', + discoveryApi, + }), + ); + }} + > + Enter + + } + > + Sign in as a Guest. + + + ); +}; -const loader: ProviderLoader = async () => { - return new GuestUserIdentity(); +const loader: ProviderLoader = async apis => { + const identity = new ProxiedSignInIdentity({ + provider: 'guest', + discoveryApi: apis.get(discoveryApiRef)!, + }); + + await identity.start(); + + const identityResponse = await identity.getBackstageIdentity(); + + if (!identityResponse) { + return undefined; + } + + return identity; }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 20613b7509..e456a6948b 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -43,7 +43,6 @@ export type SignInProviderType = { }; const signInProviders: { [key: string]: SignInProvider } = { - /** @deprecated Use `@backstage/plugin-auth-backend-module-guest-provider` */ guest: guestProvider, custom: customProvider, common: commonProvider, diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index b11352b373..d89544cf68 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -469,15 +469,3 @@ export const vmwareCloudAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.vmware-cloud', }); - -/** - * Provides guest authentication support. - * - * @public - * @remarks - */ -export const guestAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.guest', -}); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index af9bfaf751..70f8cfa2ae 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -292,7 +292,6 @@ describe('createApp', () => { - ] " diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md index 471b522859..20c1eb2f15 100644 --- a/plugins/auth-backend-module-guest-provider/README.md +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -2,7 +2,7 @@ This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state. -**NOTE**: This provider should only ever be enabled for `development` or `test`. Enabling this for production is strongly discouraged as it would give everyone a way to bypass your other authentication methods. +**NOTE**: This provider should only ever be enabled for `development`. This package is explicitly disabled for non-development environments. ## Installation @@ -20,42 +20,15 @@ const backend = createBackend(); backend.start(); ``` -#### Old Backend - -This module was also backported for the old backend and can be used like so, - -```diff -+import { -+ providers, -+} from '@backstage/plugin-auth-backend'; - .... - return await createRouter({ - ... - providerFactories: { - gitlab: providers.gitlab(), -+ guest: providers.guest(), - ... - } - ... -``` - ### Frontend Add the following to your `SignInPage` providers, ```diff -+import { -+ guestAuthApiRef, -+} from '@backstage/core-plugin-api'; - const providers = [ -+ { -+ id: 'guest-auth-provider', -+ title: 'Guest', -+ message: 'Sign in as a guest', -+ apiRef: guestAuthApiRef, -+ }, ++ 'guest', ... +] ``` ### Config diff --git a/plugins/auth-backend/src/providers/guest/index.ts b/plugins/auth-backend-module-guest-provider/src/authenticator.ts similarity index 67% rename from plugins/auth-backend/src/providers/guest/index.ts rename to plugins/auth-backend-module-guest-provider/src/authenticator.ts index 7b384798b0..d33fc2c7c9 100644 --- a/plugins/auth-backend/src/providers/guest/index.ts +++ b/plugins/auth-backend-module-guest-provider/src/authenticator.ts @@ -1,3 +1,5 @@ +import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; + /* * Copyright 2024 The Backstage Authors * @@ -13,4 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { guest } from './provider'; +export const guestAuthenticator = createProxyAuthenticator({ + defaultProfileTransform: async () => { + return { profile: {} }; + }, + initialize() {}, + async authenticate() { + return { result: {} }; + }, +}); diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts deleted file mode 100644 index acd08940a3..0000000000 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthFactory.ts +++ /dev/null @@ -1,59 +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. - */ - -import { SignInResolverFactory } from '@backstage/plugin-auth-node'; -import type { - AuthProviderFactory, - ProfileTransform, - SignInResolver, -} from '@backstage/plugin-auth-node'; -import { createGuestAuthRouteHandlers } from './createGuestAuthRouteHandlers'; -import { guestResolver } from './resolvers'; - -const defaultTransform: ProfileTransform<{}> = async () => { - return { - profile: { - displayName: 'Guest', - }, - }; -}; - -/** @public */ -export function createGuestAuthProviderFactory(options?: { - profileTransform?: ProfileTransform<{}>; - signInResolver?: SignInResolver<{}>; - signInResolverFactories?: Record>; -}): AuthProviderFactory { - return ctx => { - const signInResolver = options?.signInResolver ?? guestResolver(); - - if (!signInResolver) { - throw new Error( - `No sign-in resolver configured for guest auth provider '${ctx.providerId}'`, - ); - } - const profileTransform = options?.profileTransform ?? defaultTransform; - - return createGuestAuthRouteHandlers({ - signInResolver, - baseUrl: ctx.baseUrl, - appUrl: ctx.appUrl, - config: ctx.config, - resolverContext: ctx.resolverContext, - profileTransform, - }); - }; -} diff --git a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts b/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts deleted file mode 100644 index 76a69ca75b..0000000000 --- a/plugins/auth-backend-module-guest-provider/src/createGuestAuthRouteHandlers.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Request, Response } from 'express'; -import type { Config } from '@backstage/config'; -import { - AuthProviderRouteHandlers, - AuthResolverContext, - ClientAuthResponse, - ProfileTransform, - SignInResolver, - prepareBackstageIdentityResponse, - sendWebMessageResponse, -} from '@backstage/plugin-auth-node'; - -export interface GuestAuthRouteHandlersOptions { - config: Config; - baseUrl: string; - appUrl: string; - resolverContext: AuthResolverContext; - signInResolver: SignInResolver<{}>; - profileTransform: ProfileTransform<{}>; -} - -export function createGuestAuthRouteHandlers( - options: GuestAuthRouteHandlersOptions, -): AuthProviderRouteHandlers { - const { resolverContext, signInResolver, appUrl, profileTransform } = options; - - const createGuestSession = async (): Promise> => { - const { profile } = await profileTransform({}, resolverContext); - - const identity = await signInResolver( - { profile, result: {} }, - resolverContext, - ); - - return { - profile, - providerInfo: {}, - backstageIdentity: prepareBackstageIdentityResponse(identity), - }; - }; - - return { - async start(_, res): Promise { - // We are the auth provider for guests, skip this step. - res.redirect('handler/frame'); - }, - - /** - * This is where we create the token for the guest user. You can override the - * entityRef for the guest user with `signInResolver`. - */ - async frameHandler(_, res): Promise { - const session = await createGuestSession(); - // post message back to popup if successful - sendWebMessageResponse(res, appUrl, { - type: 'authorization_response', - response: session, - }); - }, - - /** - * Support refreshing the guest user's token. This should just improve the experience of - * browsing while in guest mode. - */ - async refresh(this: never, _: Request, res: Response): Promise { - const session = await createGuestSession(); - res.status(200).json(session); - }, - - async logout(_, res) { - // If we don't send a response or it gets cached into a 204, the page will hang. - res.end(); - }, - }; -} diff --git a/plugins/auth-backend-module-guest-provider/src/index.ts b/plugins/auth-backend-module-guest-provider/src/index.ts index 0c4a382a87..c6ada31c3a 100644 --- a/plugins/auth-backend-module-guest-provider/src/index.ts +++ b/plugins/auth-backend-module-guest-provider/src/index.ts @@ -20,5 +20,4 @@ * @packageDocumentation */ -export { createGuestAuthProviderFactory } from './createGuestAuthFactory'; export { authModuleGuestProvider as default } from './module'; diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index 4d191ac9e1..a52b8a4ac2 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -17,8 +17,12 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { authProvidersExtensionPoint } from '@backstage/plugin-auth-node'; -import { createGuestAuthProviderFactory } from './createGuestAuthFactory'; +import { + authProvidersExtensionPoint, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { guestAuthenticator } from './authenticator'; +import { signInAsGuestUser } from './resolvers'; /** @public */ export const authModuleGuestProvider = createBackendModule({ @@ -31,14 +35,17 @@ export const authModuleGuestProvider = createBackendModule({ providers: authProvidersExtensionPoint, }, async init({ providers }) { - if (process.env.NODE_ENV === 'production') { + if (process.env.NODE_ENV !== 'development') { throw new Error( 'Guest provider does not support authenticating production workloads.', ); } providers.registerProvider({ providerId: 'guest', - factory: createGuestAuthProviderFactory(), + factory: createProxyAuthProviderFactory({ + authenticator: guestAuthenticator, + signInResolver: signInAsGuestUser, + }), }); }, }); diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts index 5922530c5c..05acf676ec 100644 --- a/plugins/auth-backend-module-guest-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -15,7 +15,7 @@ */ import { stringifyEntityRef } from '@backstage/catalog-model'; -import { createSignInResolverFactory } from '@backstage/plugin-auth-node'; +import { SignInResolver } from '@backstage/plugin-auth-node'; /** * Provide a default implementation of the user to resolve to. By default, this @@ -23,24 +23,20 @@ import { createSignInResolverFactory } from '@backstage/plugin-auth-node'; * catalog. If that user doesn't exist in the catalog, we will still create a * token for them so they can keep viewing. */ -export const guestResolver = createSignInResolverFactory({ - create() { - return async (_, ctx) => { - const userRef = stringifyEntityRef({ - kind: 'user', - name: 'guest', - }); - try { - return ctx.signInWithCatalogUser({ entityRef: userRef }); - } catch (err) { - // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, - return ctx.issueToken({ - claims: { - sub: userRef, - ent: [userRef], - }, - }); - } - }; - }, -}); +export const signInAsGuestUser: SignInResolver<{}> = async (_, ctx) => { + const userRef = stringifyEntityRef({ + kind: 'user', + name: 'guest', + }); + try { + return ctx.signInWithCatalogUser({ entityRef: userRef }); + } catch (err) { + // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, + return ctx.issueToken({ + claims: { + sub: userRef, + ent: [userRef], + }, + }); + } +}; diff --git a/plugins/auth-backend/src/providers/guest/provider.ts b/plugins/auth-backend/src/providers/guest/provider.ts deleted file mode 100644 index 2efc584d5d..0000000000 --- a/plugins/auth-backend/src/providers/guest/provider.ts +++ /dev/null @@ -1,48 +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 { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { AuthHandler, SignInResolver } from '../types'; -import { createGuestAuthProviderFactory } from '@backstage/plugin-auth-backend-module-guest-provider'; - -/** - * Auth provider integration for Google auth - * - * @public - */ -export const guest = createAuthProviderIntegration({ - create(options?: { - /** - * The profile transformation function used to verify and convert the auth response - * into the profile that will be presented to the user. - */ - authHandler?: AuthHandler<{}>; - - /** - * Configure sign-in for this provider, without it the provider can not be used to sign users in. - */ - signIn?: { - /** - * Maps an auth result to a Backstage identity for the user. - */ - resolver: SignInResolver<{}>; - }; - }) { - return createGuestAuthProviderFactory({ - profileTransform: options?.authHandler, - signInResolver: options?.signIn?.resolver, - }); - }, -}); diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index d527bf8b13..76ac51f662 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -30,7 +30,6 @@ import { oidc } from './oidc'; import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; -import { guest } from './guest'; import { bitbucketServer } from './bitbucketServer'; import { easyAuth } from './azure-easyauth'; import { AuthProviderFactory } from '@backstage/plugin-auth-node'; @@ -59,7 +58,6 @@ export const providers = Object.freeze({ onelogin, saml, easyAuth, - guest, }); /** @@ -85,5 +83,4 @@ export const defaultAuthProviderFactories: { bitbucket: bitbucket.create(), bitbucketServer: bitbucketServer.create(), atlassian: atlassian.create(), - guest: guest.create(), }; diff --git a/yarn.lock b/yarn.lock index 769d1f52a3..7e674683b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -27411,6 +27414,7 @@ __metadata: "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-backend": "workspace:^" "@backstage/plugin-badges-backend": "workspace:^" @@ -37347,7 +37351,7 @@ __metadata: languageName: node linkType: hard -"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.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": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -37360,19 +37364,6 @@ __metadata: languageName: node linkType: hard -"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": - version: 1.7.0 - resolution: "passport-oauth2@npm:1.7.0" - dependencies: - base64url: 3.x.x - oauth: 0.10.x - passport-strategy: 1.x.x - uid2: 0.0.x - utils-merge: 1.x.x - checksum: a9a80b968343c9c1906f74ef613b346ec2d6a6acfe17af81e673fd774779b436729252485755c3ce182f2cdba2434d75067418952d722404d65b93c0360ca02b - languageName: node - linkType: hard - "passport-oauth@npm:1.0.0, passport-oauth@npm:^1.0.0": version: 1.0.0 resolution: "passport-oauth@npm:1.0.0" From 4f4fce91cb55d9beec426c653991d17a3397266b Mon Sep 17 00:00:00 2001 From: Aramis Date: Sun, 11 Feb 2024 12:56:51 -0500 Subject: [PATCH 361/483] small fixes Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- app-config.yaml | 5 +- packages/backend/src/plugins/auth.ts | 2 - packages/core-app-api/api-report.md | 20 ------- .../RefreshingDirectAuthConnector.ts | 60 ------------------- packages/core-plugin-api/api-report.md | 5 -- .../api-report.md | 11 ---- plugins/auth-backend/api-report.md | 15 ----- 7 files changed, 1 insertion(+), 117 deletions(-) delete mode 100644 packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts diff --git a/app-config.yaml b/app-config.yaml index b864c3b610..e18c45aefa 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -400,10 +400,7 @@ auth: myproxy: development: {} guest: - development: - signIn: - resolvers: - - resolver: guestUser + development: {} costInsights: engineerCost: 200000 diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 773d3f4270..0d92315f92 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -141,8 +141,6 @@ export default async function createPlugin( }, }, }), - - guest: providers.guest.create(), }, }); } diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 0ecb0193db..8b8cc991c8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -464,26 +464,6 @@ export class GoogleAuth { static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } -// @public -export class GuestAuth - implements ProfileInfoApi, BackstageIdentityApi, SessionApi -{ - // (undocumented) - static create(options: AuthApiCreateOptions): GuestAuth; - // (undocumented) - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; - // (undocumented) - getProfile(options?: AuthRequestOptions): Promise; - // (undocumented) - sessionState$(): Observable; - // (undocumented) - signIn(): Promise; - // (undocumented) - signOut(): Promise; -} - // @public export class LocalStorageFeatureFlags implements FeatureFlagsApi { // (undocumented) diff --git a/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts deleted file mode 100644 index 94f7486210..0000000000 --- a/packages/core-app-api/src/lib/AuthConnector/RefreshingDirectAuthConnector.ts +++ /dev/null @@ -1,60 +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 { DirectAuthConnector } from './DirectAuthConnector'; - -/** - * Add support for refreshing direct tokens. Used for guest authentication. - */ -export class RefreshingDirectAuthConnector< - DirectAuthResponse, -> extends DirectAuthConnector { - /** - * Pulled from DefaultAuthConnector and adapted for use with DirectAuthConnector. - */ - async refreshSession(): Promise { - const res = await fetch( - `${await this.buildUrl('/refresh')}&optional=true`, - { - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - credentials: 'include', - }, - ).catch(error => { - throw new Error(`Auth refresh request failed, ${error}`); - }); - - if (!res.ok) { - const error: any = new Error( - `Auth refresh request failed, ${res.statusText}`, - ); - error.status = res.status; - throw error; - } - - const authInfo = await res.json(); - - if (authInfo.error) { - const error = new Error(authInfo.error.message); - if (authInfo.error.name) { - error.name = authInfo.error.name; - } - throw error; - } - return authInfo; - } -} diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index ad91a0431e..2a993687b3 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -503,11 +503,6 @@ export const googleAuthApiRef: ApiRef< SessionApi >; -// @public -export const guestAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type IconComponent = ComponentType< | { diff --git a/plugins/auth-backend-module-guest-provider/api-report.md b/plugins/auth-backend-module-guest-provider/api-report.md index adaf4313fb..773b80b9ba 100644 --- a/plugins/auth-backend-module-guest-provider/api-report.md +++ b/plugins/auth-backend-module-guest-provider/api-report.md @@ -3,20 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import type { AuthProviderFactory } from '@backstage/plugin-auth-node'; import { BackendFeature } from '@backstage/backend-plugin-api'; -import type { ProfileTransform } from '@backstage/plugin-auth-node'; -import type { SignInResolver } from '@backstage/plugin-auth-node'; -import { SignInResolverFactory } from '@backstage/plugin-auth-node'; // @public (undocumented) const authModuleGuestProvider: () => BackendFeature; export default authModuleGuestProvider; - -// @public (undocumented) -export function createGuestAuthProviderFactory(options?: { - profileTransform?: ProfileTransform<{}>; - signInResolver?: SignInResolver<{}>; - signInResolverFactories?: Record>; -}): AuthProviderFactory; ``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 431caf3e91..1beddb6419 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -644,21 +644,6 @@ export const providers: Readonly<{ ) => AuthProviderFactory_2; resolvers: never; }>; - guest: Readonly<{ - create: ( - options?: - | { - authHandler?: AuthHandler<{}> | undefined; - signIn?: - | { - resolver: SignInResolver<{}>; - } - | undefined; - } - | undefined, - ) => AuthProviderFactory_2; - resolvers: never; - }>; }>; // @public @deprecated (undocumented) From 10d56c1d7c46852baa012d0a63577fec96a9b3a4 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sun, 11 Feb 2024 13:07:55 -0500 Subject: [PATCH 362/483] more clean up Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- .changeset/gentle-starfishes-camp.md | 8 -------- .changeset/selfish-glasses-cheer.md | 10 +++++++++- plugins/auth-backend/package.json | 1 - yarn.lock | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 .changeset/gentle-starfishes-camp.md diff --git a/.changeset/gentle-starfishes-camp.md b/.changeset/gentle-starfishes-camp.md deleted file mode 100644 index 229d4c39c9..0000000000 --- a/.changeset/gentle-starfishes-camp.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/core-plugin-api': minor -'@backstage/app-defaults': minor -'@backstage/core-app-api': minor -'@backstage/plugin-auth-backend': minor ---- - -Adds in support for the new guest provider added by `@backstage/plugin-auth-backend-module-guest-provider`. diff --git a/.changeset/selfish-glasses-cheer.md b/.changeset/selfish-glasses-cheer.md index 0a56a8b489..ffd9bb74e0 100644 --- a/.changeset/selfish-glasses-cheer.md +++ b/.changeset/selfish-glasses-cheer.md @@ -2,4 +2,12 @@ '@backstage/core-components': minor --- -**DEPRECATED** `SignInPage`'s `'guest'` provider is deprecated. Use `@backstage/plugin-auth-backend-module-guest-provider` instead. +**BREAKING** `SignInPage`'s `'guest'` provider now uses `@backstage/plugin-auth-backend-module-guest-provider` to generate tokens. You must install that provider into your backend to continue using the `'guest'` option. + +```diff +const backend = createBackend(); + ++backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); + +backend.start(); +``` diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 3ea1d26475..456cc18af2 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -49,7 +49,6 @@ "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", - "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 7e674683b6..1bd1a644bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4854,7 +4854,6 @@ __metadata: "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^" "@backstage/plugin-auth-backend-module-google-provider": "workspace:^" - "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-auth-backend-module-microsoft-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^" From 4fa994f91584548ac1a270f21ccc8e5943b8e1f9 Mon Sep 17 00:00:00 2001 From: Aramis Date: Sun, 11 Feb 2024 14:34:23 -0500 Subject: [PATCH 363/483] run yarn fix Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- plugins/auth-backend-module-guest-provider/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index ae50007225..edf3210759 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -10,6 +10,11 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-guest-provider" + }, "backstage": { "role": "backend-plugin-module" }, From 9fc765af207bf9b36e95eae0b3ae2f42debb1982 Mon Sep 17 00:00:00 2001 From: Aramis Date: Mon, 12 Feb 2024 10:02:11 -0500 Subject: [PATCH 364/483] update with a guide Signed-off-by: Aramis Signed-off-by: aramissennyeydd --- docs/auth/guest/provider.md | 65 +++++++++++++++++++ microsite/sidebars.json | 1 + mkdocs.yml | 1 + .../README.md | 42 ------------ .../src/authenticator.ts | 5 +- 5 files changed, 70 insertions(+), 44 deletions(-) create mode 100644 docs/auth/guest/provider.md diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md new file mode 100644 index 0000000000..09c52fcd3b --- /dev/null +++ b/docs/auth/guest/provider.md @@ -0,0 +1,65 @@ +--- +id: provider +title: Guest Authentication Provider +sidebar_label: Guest +description: Adding a guest authentication provider in Backstage +--- + +Audience: Admins or developers + +## Summary + +The goal of this guide is to get you set up with a guest authentication provider that emits tokens. This is different than the old guest authentication that is purely stored on the frontend and does not have tokens. The main reason you'd want to use this provider is to use permissioned plugins. + +:::caution +This provider should only ever be enabled for `development`. To prevent unauthorized access to your data, this package is _explicitly_ disabled for non-development environments. +::: + +## Installation + +### Backend + +:::note +This will only work with the new backend system. There is no support for this in the old backend. +::: + +Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. + +``` +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider +``` + +Then, add it to your backend's `index.ts` file, + +```diff +const backend = createBackend(); + +backend.add('@backstage/plugin-auth-backend'); ++backend.add('@backstage/plugin-auth-backend-module-guest-provider'); + +await backend.start(); +``` + +### Frontend + +Add the following to your `SignInPage` providers, + +```diff +const providers = [ ++ 'guest', + ... +] +``` + +### Config + +Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`, + +```diff +auth: + providers: ++ guest: ++ development: {} +``` + +We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 4f2724c0b6..fd13fcb57b 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -307,6 +307,7 @@ "auth/gitlab/provider", "auth/google/provider", "auth/google/gcp-iap-auth", + "auth/guest/provider", "auth/okta/provider", "auth/oauth2-proxy/provider", "auth/onelogin/provider", diff --git a/mkdocs.yml b/mkdocs.yml index 2a17613672..a2b2749233 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,6 +161,7 @@ nav: - GitLab: 'auth/gitlab/provider.md' - Google: 'auth/google/provider.md' - Google IAP: 'auth/google/gcp-iap-auth.md' + - Guest: 'auth/guest/provider.md' - OAuth2Proxy: 'auth/oauth2-proxy/provider.md' - Okta: 'auth/okta/provider.md' - OneLogin: 'auth/onelogin/provider.md' diff --git a/plugins/auth-backend-module-guest-provider/README.md b/plugins/auth-backend-module-guest-provider/README.md index 20c1eb2f15..79591c987b 100644 --- a/plugins/auth-backend-module-guest-provider/README.md +++ b/plugins/auth-backend-module-guest-provider/README.md @@ -2,48 +2,6 @@ This module provides a guest auth provider implementation for `@backstage/plugin-auth-backend`. This is meant to supersede the existing `'guest'` option for authentication that does not emit tokens and is completely stored as frontend state. -**NOTE**: This provider should only ever be enabled for `development`. This package is explicitly disabled for non-development environments. - -## Installation - -### Backend - -#### New Backend - -```diff -const backend = createBackend(); -... - -+backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); - -... -backend.start(); -``` - -### Frontend - -Add the following to your `SignInPage` providers, - -```diff -const providers = [ -+ 'guest', - ... -] -``` - -### Config - -Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`, - -```diff -auth: - providers: -+ guest: -+ development: {} -``` - -We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. - ## Links - [Backstage](https://backstage.io) diff --git a/plugins/auth-backend-module-guest-provider/src/authenticator.ts b/plugins/auth-backend-module-guest-provider/src/authenticator.ts index d33fc2c7c9..436b72617d 100644 --- a/plugins/auth-backend-module-guest-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-guest-provider/src/authenticator.ts @@ -1,5 +1,3 @@ -import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; - /* * Copyright 2024 The Backstage Authors * @@ -15,6 +13,9 @@ import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; * See the License for the specific language governing permissions and * limitations under the License. */ + +import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; + export const guestAuthenticator = createProxyAuthenticator({ defaultProfileTransform: async () => { return { profile: {} }; From d622690f8cc944ca7b2309335891d9ff4dbfeaea Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sat, 17 Feb 2024 17:38:36 -0500 Subject: [PATCH 365/483] code review updates Signed-off-by: aramissennyeydd --- .changeset/cold-boats-sell.md | 7 +++- .changeset/selfish-glasses-cheer.md | 10 +---- app-config.yaml | 2 +- docs/auth/guest/provider.md | 4 +- .../examples/acme/team-a-group.yaml | 14 +++++++ .../templates/default-app/examples/org.yaml | 9 +++++ .../config.d.ts | 27 +++++++++++++ .../src/module.ts | 11 +++-- .../src/resolvers.ts | 40 ++++++++++--------- 9 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 plugins/auth-backend-module-guest-provider/config.d.ts diff --git a/.changeset/cold-boats-sell.md b/.changeset/cold-boats-sell.md index 112d9022bc..e47517140d 100644 --- a/.changeset/cold-boats-sell.md +++ b/.changeset/cold-boats-sell.md @@ -2,4 +2,9 @@ '@backstage/plugin-auth-backend-module-guest-provider': patch --- -Adds a new guest provider that maps guest users to actual tokens. +Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.guestEntityRef` config key) like so, + +```yaml title=app-config.yaml +auth: + guestEntityRef: user:default/guest +``` diff --git a/.changeset/selfish-glasses-cheer.md b/.changeset/selfish-glasses-cheer.md index ffd9bb74e0..4d880f0523 100644 --- a/.changeset/selfish-glasses-cheer.md +++ b/.changeset/selfish-glasses-cheer.md @@ -2,12 +2,4 @@ '@backstage/core-components': minor --- -**BREAKING** `SignInPage`'s `'guest'` provider now uses `@backstage/plugin-auth-backend-module-guest-provider` to generate tokens. You must install that provider into your backend to continue using the `'guest'` option. - -```diff -const backend = createBackend(); - -+backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); - -backend.start(); -``` +`SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback. diff --git a/app-config.yaml b/app-config.yaml index e18c45aefa..4b92687653 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -242,7 +242,7 @@ catalog: - Domain - Location providers: - openapi: + backstageOpenapi: plugins: - catalog - search diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index 09c52fcd3b..ca5bee1d09 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -59,7 +59,9 @@ Similar to the other authentication providers, you have to enable the provider i auth: providers: + guest: -+ development: {} ++ development: + // new optional property to override the default value. ++ loginAs: user:default/guest ``` We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml index 7fe0e7b3f3..eb95c47093 100644 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -57,3 +57,17 @@ spec: displayName: Guest User email: guest@example.com memberOf: [team-a] +--- +# This user is added as an example, to make it more easy for the "Guest" +# sign-in option to demonstrate some entities being owned. In a regular org, +# a guest user would probably not be registered like this. +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest + namespace: development +spec: + profile: + displayName: Guest User + email: guest@example.com + memberOf: [group:default/team-a] diff --git a/packages/create-app/templates/default-app/examples/org.yaml b/packages/create-app/templates/default-app/examples/org.yaml index a10e81fc7f..1c4fb91a1e 100644 --- a/packages/create-app/templates/default-app/examples/org.yaml +++ b/packages/create-app/templates/default-app/examples/org.yaml @@ -7,6 +7,15 @@ metadata: spec: memberOf: [guests] --- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest + namespace: development +spec: + memberOf: [guests] +--- # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group apiVersion: backstage.io/v1alpha1 kind: Group diff --git a/plugins/auth-backend-module-guest-provider/config.d.ts b/plugins/auth-backend-module-guest-provider/config.d.ts new file mode 100644 index 0000000000..d6de29bed6 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/config.d.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export interface Config { + /** Configuration options for the auth plugin */ + auth?: { + /** + * EXPERIMENTAL value: Allow users to configure what the guest provider logs in as. + * @visibility frontend + * @default user:default/guest + */ + guestEntityRef?: string; + }; +} diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index a52b8a4ac2..75eb4f849c 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -33,18 +33,21 @@ export const authModuleGuestProvider = createBackendModule({ deps: { logger: coreServices.logger, providers: authProvidersExtensionPoint, + config: coreServices.rootConfig, }, - async init({ providers }) { + async init({ providers, logger, config }) { if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Guest provider does not support authenticating production workloads.', + logger.warn( + 'You should NOT be using the guest provider outside of a development environment.', ); } providers.registerProvider({ providerId: 'guest', factory: createProxyAuthProviderFactory({ authenticator: guestAuthenticator, - signInResolver: signInAsGuestUser, + signInResolver: signInAsGuestUser( + config.getOptionalString('auth.guestEntityRef'), + ), }), }); }, diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts index 05acf676ec..b2c2f8cf7d 100644 --- a/plugins/auth-backend-module-guest-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -19,24 +19,28 @@ import { SignInResolver } from '@backstage/plugin-auth-node'; /** * Provide a default implementation of the user to resolve to. By default, this - * is `user:default/guest`. We will attempt to get that user if they're in the + * is `user:development/guest`. We will attempt to get that user if they're in the * catalog. If that user doesn't exist in the catalog, we will still create a * token for them so they can keep viewing. */ -export const signInAsGuestUser: SignInResolver<{}> = async (_, ctx) => { - const userRef = stringifyEntityRef({ - kind: 'user', - name: 'guest', - }); - try { - return ctx.signInWithCatalogUser({ entityRef: userRef }); - } catch (err) { - // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, - return ctx.issueToken({ - claims: { - sub: userRef, - ent: [userRef], - }, - }); - } -}; +export const signInAsGuestUser: (entityRef?: string) => SignInResolver<{}> = + (entityRef?: string) => async (_, ctx) => { + const userRef = + entityRef ?? + stringifyEntityRef({ + kind: 'user', + namespace: 'development', + name: 'guest', + }); + try { + return ctx.signInWithCatalogUser({ entityRef: userRef }); + } catch (err) { + // We can't guarantee that a guest user exists in the catalog, so we issue a token directly, + return ctx.issueToken({ + claims: { + sub: userRef, + ent: [userRef], + }, + }); + } + }; From 24cc1a472a8107e893f98f90c9577e780f333c47 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 18 Feb 2024 00:20:19 -0500 Subject: [PATCH 366/483] update guest provider to support both old and new guest sessions Signed-off-by: aramissennyeydd --- .../src/layout/SignInPage/guestProvider.tsx | 81 ++++++++++++++----- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 018feb2241..a2ec2c1078 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -22,28 +22,61 @@ import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { ProxiedSignInIdentity } from '../ProxiedSignInPage/ProxiedSignInIdentity'; import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; +import { GuestUserIdentity } from './GuestUserIdentity'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { ResponseError } from '@backstage/errors'; + +const getIdentity = async (identity: ProxiedSignInIdentity) => { + try { + const identityResponse = await identity.getBackstageIdentity(); + return identityResponse; + } catch (error) { + if ( + error instanceof ResponseError && + error.cause.name === 'NotFoundError' + ) { + return undefined; + } + throw error; + } +}; const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { const discoveryApi = useApi(discoveryApiRef); + const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken'); + + const handle = async () => { + onSignInStarted(); + + const identity = new ProxiedSignInIdentity({ + provider: 'guest', + discoveryApi, + }); + + const identityResponse = await getIdentity(identity); + + if (!identityResponse) { + // eslint-disable-next-line no-alert + const useLegacyGuestTokenResponse = confirm( + 'Failed to sign in as a guest using the auth backend. Do you want to fallback to the legacy guest token?', + ); + if (useLegacyGuestTokenResponse) { + setUseLegacyGuestToken(true); + onSignInSuccess(new GuestUserIdentity()); + return; + } + } + + onSignInSuccess(identity); + }; + return ( { - onSignInStarted(); - onSignInSuccess( - new ProxiedSignInIdentity({ - provider: 'guest', - discoveryApi, - }), - ); - }} - > + } @@ -55,17 +88,29 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { }; const loader: ProviderLoader = async apis => { + const useLegacyGuestToken = + localStorage.getItem('enableLegacyGuestToken') === 'true'; + const identity = new ProxiedSignInIdentity({ provider: 'guest', discoveryApi: apis.get(discoveryApiRef)!, }); + const identityResponse = await getIdentity(identity); - await identity.start(); - - const identityResponse = await identity.getBackstageIdentity(); - - if (!identityResponse) { + if (!identityResponse && !useLegacyGuestToken) { return undefined; + } else if (identityResponse && useLegacyGuestToken) { + // eslint-disable-next-line no-alert + const switchToNewGuestToken = confirm( + 'You are currently using the legacy guest token, but you have the new guest backend module installed. Do you want to use the new module?', + ); + if (switchToNewGuestToken) { + localStorage.removeItem('enableLegacyGuestToken'); + } else { + return new GuestUserIdentity(); + } + } else if (useLegacyGuestToken) { + return new GuestUserIdentity(); } return identity; From 6f2fbff528867aae12540dfc9abd949bb9ed0db1 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 18 Feb 2024 00:35:37 -0500 Subject: [PATCH 367/483] add signin failure error Signed-off-by: aramissennyeydd --- .../src/layout/SignInPage/guestProvider.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index a2ec2c1078..adb5f4b027 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -41,7 +41,11 @@ const getIdentity = async (identity: ProxiedSignInIdentity) => { } }; -const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { +const Component: ProviderComponent = ({ + onSignInStarted, + onSignInSuccess, + onSignInFailure, +}) => { const discoveryApi = useApi(discoveryApiRef); const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken'); @@ -65,6 +69,10 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { onSignInSuccess(new GuestUserIdentity()); return; } + onSignInFailure(); + throw new Error( + `You cannot sign in as a guest, you must either enable the legacy guest token or configure the auth backend to support guest sign in.`, + ); } onSignInSuccess(identity); From 9332425e1c6268b6f0c616d009dcb88269469c7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 16:37:10 +0100 Subject: [PATCH 368/483] catalog: fix alpha entity 404 Signed-off-by: Patrik Oldsberg --- .changeset/lemon-lemons-sparkle.md | 5 +++++ plugins/catalog/src/alpha/pages.tsx | 31 +++++++++++++++-------------- 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 .changeset/lemon-lemons-sparkle.md diff --git a/.changeset/lemon-lemons-sparkle.md b/.changeset/lemon-lemons-sparkle.md new file mode 100644 index 0000000000..6aae1a7f8d --- /dev/null +++ b/.changeset/lemon-lemons-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The entity page extension provided by the `/alpha` plugin now correctly renders the entity 404 page. diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 15f7d6cc91..5c583e6a19 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -65,22 +65,23 @@ export const catalogEntityPage = createPageExtension({ loader: async ({ inputs }) => { const { EntityLayout } = await import('../components/EntityLayout'); const Component = () => { - const { entity, ...rest } = useEntityFromUrl(); return ( - - {entity ? ( - - {inputs.contents - .filter(({ output: { filterFunction, filterExpression } }) => - buildFilterFn(filterFunction, filterExpression)(entity), - ) - .map(({ output: { path, title, element } }) => ( - - {element} - - ))} - - ) : null} + + + {inputs.contents.map(({ output }) => ( + + {output.element} + + ))} + ); }; From 4ba74478473b50e4a1e469cdf5f4e7c8b50268b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Feb 2024 16:38:20 +0100 Subject: [PATCH 369/483] Update plugins/auth-backend/config.d.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/auth-backend/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 0a1425c98b..f5f4dd29fd 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -185,7 +185,7 @@ export interface Config { /** @visibility frontend */ cfaccess?: { teamName: string; - /** @visibility secret */ + /** @deepVisibility secret */ serviceTokens?: Array<{ token: string; subject: string; From aebb8dc87317873e9631c6a636f9ab06fcfddcee Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 26 Feb 2024 16:42:10 +0100 Subject: [PATCH 370/483] chore: update changeset Signed-off-by: blam --- .changeset/clever-eagles-boil.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/clever-eagles-boil.md b/.changeset/clever-eagles-boil.md index 6e69d2db2f..a2f47ed092 100644 --- a/.changeset/clever-eagles-boil.md +++ b/.changeset/clever-eagles-boil.md @@ -1,5 +1,6 @@ --- '@backstage/plugin-adr': patch +'@backstage/plugin-adr-common': patch --- -Fixed Azure DevOps ADR file path +Fixed Azure DevOps ADR file path reading From cceebae5ac8f37949262d297cb400499482cbaa7 Mon Sep 17 00:00:00 2001 From: Rickard Dybeck Date: Mon, 26 Feb 2024 10:50:54 -0500 Subject: [PATCH 371/483] [code-coverage] fix jacoco to not require scm-only Currently the jacoco plugin only works if you have the annotation set to scm-only. Signed-off-by: Rickard Dybeck --- .changeset/empty-wolves-rule.md | 5 +++++ .../src/service/converter/jacoco.test.ts | 6 ++++++ .../code-coverage-backend/src/service/converter/jacoco.ts | 8 +++++--- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .changeset/empty-wolves-rule.md diff --git a/.changeset/empty-wolves-rule.md b/.changeset/empty-wolves-rule.md new file mode 100644 index 0000000000..a549cf3628 --- /dev/null +++ b/.changeset/empty-wolves-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Fix jacoco convertor to not require annotation to be set to scm-only. diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts index 80de585d44..b6b43234e6 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.test.ts @@ -54,4 +54,10 @@ describe('convert jacoco', () => { expect(files.sort()).toEqual(expected.sort()); }); + + it('works when not providing files (as per not setting annotation to scm-only)', () => { + const files = converter.convert(fixture, []); + + expect(files).toHaveLength(4); + }); }); diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.ts index ac5ccbfa45..d461cfe7d7 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.ts @@ -40,7 +40,6 @@ export class Jacoco implements Converter { */ convert(xml: JacocoXML, scmFiles: Array): Array { const jscov: Array = []; - xml.report.package.forEach(r => { const packageName = r.$.name; r.sourcefile.forEach(sf => { @@ -68,9 +67,12 @@ export class Jacoco implements Converter { .map(f => f.trimEnd()) .find(f => f.endsWith(packageAndFilename)); this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`); - if (Object.keys(lineHits).length > 0 && currentFile) { + if ( + scmFiles.length === 0 || + (Object.keys(lineHits).length > 0 && currentFile) + ) { jscov.push({ - filename: currentFile, + filename: currentFile || packageAndFilename, branchHits: branchHits, lineHits: lineHits, }); From 6f2442e977fd5db75c0c2e57598175a995007b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Feb 2024 16:34:04 +0100 Subject: [PATCH 372/483] Update plugins/azure-sites-backend/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/azure-sites-backend/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 554a3d0cc4..18c51239c3 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -51,17 +51,17 @@ Here's how to get the backend plugin up and running: } from '@backstage/plugin-azure-sites-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; - import { CatalogClient } from '@backstage/catalog-client' + import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin( env: PluginEnvironment, ): Promise { + const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); return await createRouter({ - const catalogApi = new CatalogClient({ discoveryApi: env.discovery }) logger: env.logger, azureSitesApi: AzureSitesApi.fromConfig(env.config), permissions: env.permissions, - catalogApi + catalogApi, }); } ``` From 4b277033714facd9144c9ea93ed78b0a3114f812 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:59:01 -0500 Subject: [PATCH 373/483] Apply suggestions from code review Co-authored-by: Patrik Oldsberg Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- .changeset/cold-boats-sell.md | 2 +- .changeset/selfish-glasses-cheer.md | 2 +- .../core-components/src/layout/SignInPage/guestProvider.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/cold-boats-sell.md b/.changeset/cold-boats-sell.md index e47517140d..af0dd1e295 100644 --- a/.changeset/cold-boats-sell.md +++ b/.changeset/cold-boats-sell.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-guest-provider': patch +'@backstage/plugin-auth-backend-module-guest-provider': minor --- Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.guestEntityRef` config key) like so, diff --git a/.changeset/selfish-glasses-cheer.md b/.changeset/selfish-glasses-cheer.md index 4d880f0523..be267eb0f3 100644 --- a/.changeset/selfish-glasses-cheer.md +++ b/.changeset/selfish-glasses-cheer.md @@ -1,5 +1,5 @@ --- -'@backstage/core-components': minor +'@backstage/core-components': patch --- `SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback. diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index adb5f4b027..563d1cc124 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -32,8 +32,8 @@ const getIdentity = async (identity: ProxiedSignInIdentity) => { return identityResponse; } catch (error) { if ( - error instanceof ResponseError && - error.cause.name === 'NotFoundError' + error.name === 'ResponseError' && + (error as ResponseError).cause.name === 'NotFoundError' ) { return undefined; } From 62c581e86d30f7ba2adcaf545cc7f0801ae7315a Mon Sep 17 00:00:00 2001 From: Sameer Vohra Date: Mon, 26 Feb 2024 12:00:45 -0500 Subject: [PATCH 374/483] Update versioning-policy.md fix minor typo Signed-off-by: Sameer Vohra --- docs/overview/versioning-policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 2ff3588af1..8b12138499 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -34,7 +34,7 @@ their own release cadence and versioning policy. Release cadence: Monthly, specifically on the Tuesday before the third Wednesday of each month. The first release took place in March 2022. -The main release line in versioned with a major, minor and patch version but +The main release line is versioned with a major, minor and patch version but does **not** adhere to [semver](https://semver.org). The version format is `..`, for example `1.3.0`. From f8b8e2fe6f0adcbba3c8eb43192e671df1473e67 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 26 Feb 2024 18:48:05 +0100 Subject: [PATCH 375/483] refactor: move to core components and rename it Signed-off-by: Camila Belo --- .changeset/friendly-news-sin.md | 4 +- .changeset/red-taxis-swim.md | 2 +- packages/core-compat-api/api-report.md | 11 --- packages/core-compat-api/package.json | 1 - .../src/components/SystemIcon.tsx | 78 ------------------- .../core-compat-api/src/components/index.ts | 17 ---- packages/core-compat-api/src/index.ts | 2 - packages/core-components/api-report.md | 14 +++- .../src/icons/icons.test.tsx} | 26 +++---- packages/core-components/src/icons/icons.tsx | 53 +++++++++---- plugins/api-docs/src/alpha.tsx | 4 +- yarn.lock | 1 - 12 files changed, 66 insertions(+), 147 deletions(-) delete mode 100644 packages/core-compat-api/src/components/SystemIcon.tsx delete mode 100644 packages/core-compat-api/src/components/index.ts rename packages/{core-compat-api/src/components/SystemIcon.test.tsx => core-components/src/icons/icons.test.tsx} (59%) diff --git a/.changeset/friendly-news-sin.md b/.changeset/friendly-news-sin.md index ca337370af..29843c54fb 100644 --- a/.changeset/friendly-news-sin.md +++ b/.changeset/friendly-news-sin.md @@ -1,5 +1,5 @@ --- -'@backstage/core-compat-api': patch +'@backstage/core-components': minor --- -Create an abstraction to consume legacy system icons in new system extensions. +Create a component abstraction to consume system icons. diff --git a/.changeset/red-taxis-swim.md b/.changeset/red-taxis-swim.md index 7cd2428fac..13c758d552 100644 --- a/.changeset/red-taxis-swim.md +++ b/.changeset/red-taxis-swim.md @@ -2,4 +2,4 @@ '@backstage/plugin-api-docs': patch --- -Use the system icon compatibility component in the navigation item extension. +Use the `AppIcon` component in the navigation item extension. diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 82f13f1482..be80ca50e9 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -8,11 +8,9 @@ import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/core-plugin-api'; -import { ComponentProps } from 'react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; -import { IconComponent } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -71,15 +69,6 @@ export class NoOpAnalyticsApi implements AnalyticsApi, AnalyticsApi_2 { captureEvent(_event: AnalyticsEvent | AnalyticsEvent_2): void; } -// @public -export function SystemIcon(props: SystemIconProps): React_2.JSX.Element; - -// @public -export type SystemIconProps = ComponentProps & { - keys: string | string[]; - Fallback?: IconComponent; -}; - // @public export type ToNewRouteRef = T extends RouteRef diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 21bf8ede49..ee70bc8089 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -45,7 +45,6 @@ "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-puppetdb": "workspace:^", "@backstage/plugin-stackstorm": "workspace:^", - "@backstage/test-utils": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.8.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0" diff --git a/packages/core-compat-api/src/components/SystemIcon.tsx b/packages/core-compat-api/src/components/SystemIcon.tsx deleted file mode 100644 index 7aa7110a97..0000000000 --- a/packages/core-compat-api/src/components/SystemIcon.tsx +++ /dev/null @@ -1,78 +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 React, { ComponentProps } from 'react'; -import { useApp, IconComponent } from '@backstage/core-plugin-api'; -import { compatWrapper } from '../compatWrapper'; - -/** - * @public - * Props for the SystemIcon component. - */ -export type SystemIconProps = ComponentProps & { - // The id of the system icon to render, if provided as an array, the first icon found will be rendered. - keys: string | string[]; - // An optional fallback icon component to render when the system icon is not found. - // Default to () => null. - Fallback?: IconComponent; -}; - -function SystemIcon(props: SystemIconProps) { - const { keys, Fallback = () => null, ...rest } = props; - const app = useApp(); - for (const key of Array.isArray(keys) ? keys : [keys]) { - const Icon = app.getSystemIcon(key); - if (Icon) return ; - } - return ; -} - -/** - * @public - * SystemIcon is a component that renders a system icon by its id. - * @example - * Rendering the "kind:api" icon: - * ```tsx - * - * ``` - * @example - * Providing multiple icon ids: - * ```tsx - * - * ``` - * @example - * Customizing the fallback icon: - * ```tsx - * - * ``` - * @example - * Customizing the icon font size: - * ```tsx - * - * ``` - */ -function CompatSystemIcon(props: SystemIconProps) { - try { - // Check if the app context is available - useApp(); - return ; - } catch { - // Fallback to the compat wrapper if the app context is not available - return compatWrapper(); - } -} - -export { CompatSystemIcon as SystemIcon }; diff --git a/packages/core-compat-api/src/components/index.ts b/packages/core-compat-api/src/components/index.ts deleted file mode 100644 index e02ab727c6..0000000000 --- a/packages/core-compat-api/src/components/index.ts +++ /dev/null @@ -1,17 +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. - */ - -export { SystemIcon, type SystemIconProps } from './SystemIcon'; diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 3da227e554..88e1892eac 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -17,8 +17,6 @@ export * from './compatWrapper'; export * from './apis'; -export * from './components'; - export { convertLegacyApp } from './convertLegacyApp'; export { convertLegacyRouteRef, diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 27b9c62653..7641386176 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -66,6 +66,15 @@ export type AlertDisplayProps = { transientTimeoutMs?: number; }; +// @public +export function AppIcon(props: AppIconProps): React_2.JSX.Element; + +// @public +export type AppIconProps = IconComponentProps & { + id: string; + Fallback?: IconComponent; +}; + // @public export const AutoLogout: (props: AutoLogoutProps) => JSX.Element | null; @@ -130,8 +139,6 @@ export type BreadcrumbsClickableTextClassKey = 'root'; // @public (undocumented) export type BreadcrumbsStyledBoxClassKey = 'root'; -// Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts -// // @public export function BrokenImageIcon(props: IconComponentProps): React_2.JSX.Element; @@ -541,6 +548,9 @@ export type HorizontalScrollGridClassKey = | 'buttonLeft' | 'buttonRight'; +// @public +export type IconComponentProps = ComponentProps; + // @public (undocumented) export function IconLinkVertical({ color, diff --git a/packages/core-compat-api/src/components/SystemIcon.test.tsx b/packages/core-components/src/icons/icons.test.tsx similarity index 59% rename from packages/core-compat-api/src/components/SystemIcon.test.tsx rename to packages/core-components/src/icons/icons.test.tsx index 12b8f41ea5..7cd54f37d0 100644 --- a/packages/core-compat-api/src/components/SystemIcon.test.tsx +++ b/packages/core-components/src/icons/icons.test.tsx @@ -17,24 +17,24 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; -import { SystemIcon } from './SystemIcon'; +import { AppIcon } from './icons'; -describe('SystemIcon', () => { +describe('AppIcon', () => { it('should render the correct system icon', async () => { - const { container } = await renderInTestApp(); - expect(container.querySelector('svg')).toBeDefined(); + await renderInTestApp(); + expect(screen.getByTestId('Api Icon')).toBeDefined(); }); - it('should render the first found icon when multiple keys are provided', async () => { - const { container } = await renderInTestApp( - , - ); - expect(container.querySelector('svg')).toBeDefined(); - }); - - it('should render the fallback component when no system icon is found', async () => { + it('should render the default fallback component', async () => { await renderInTestApp( -
Fallback Icon
} />, + , + ); + expect(screen.getByTestId('Fallback Icon')).toBeDefined(); + }); + + it('should render the custom fallback component', async () => { + await renderInTestApp( +
Fallback Icon
} />, ); expect(screen.getByText('Fallback Icon')).toBeInTheDocument(); }); diff --git a/packages/core-components/src/icons/icons.tsx b/packages/core-components/src/icons/icons.tsx index 8d22af4664..ca3b6fa76a 100644 --- a/packages/core-components/src/icons/icons.tsx +++ b/packages/core-components/src/icons/icons.tsx @@ -18,61 +18,80 @@ import { IconComponent, useApp } from '@backstage/core-plugin-api'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import React, { ComponentProps } from 'react'; -type IconComponentProps = ComponentProps; +/** + * @public + * Props for the {@link @backstage/core-plugin-api#IconComponent} component. + */ +export type IconComponentProps = ComponentProps; -function useSystemIcon(key: string, props: IconComponentProps) { +/** + * @public + * Props for the {@link AppIcon} component. + */ +export type AppIconProps = IconComponentProps & { + // The key of the system icon to render. + id: string; + // An optional fallback icon component to render when the system icon is not found. + // Default to () => null. + Fallback?: IconComponent; +}; + +/** + * @public + * A component that renders a system icon by its id. + */ +export function AppIcon(props: AppIconProps) { + const { id: key, Fallback = MuiBrokenImageIcon, ...rest } = props; const app = useApp(); - const Icon = app.getSystemIcon(key); - return Icon ? : ; + const Icon = app.getSystemIcon(key) ?? Fallback; + return ; } // Should match the list of overridable system icon keys in @backstage/core-app-api /** * Broken Image Icon - * * @public - * */ export function BrokenImageIcon(props: IconComponentProps) { - return useSystemIcon('brokenImage', props); + return ; } /** @public */ export function CatalogIcon(props: IconComponentProps) { - return useSystemIcon('catalog', props); + return ; } /** @public */ export function ChatIcon(props: IconComponentProps) { - return useSystemIcon('chat', props); + return ; } /** @public */ export function DashboardIcon(props: IconComponentProps) { - return useSystemIcon('dashboard', props); + return ; } /** @public */ export function DocsIcon(props: IconComponentProps) { - return useSystemIcon('docs', props); + return ; } /** @public */ export function EmailIcon(props: IconComponentProps) { - return useSystemIcon('email', props); + return ; } /** @public */ export function GitHubIcon(props: IconComponentProps) { - return useSystemIcon('github', props); + return ; } /** @public */ export function GroupIcon(props: IconComponentProps) { - return useSystemIcon('group', props); + return ; } /** @public */ export function HelpIcon(props: IconComponentProps) { - return useSystemIcon('help', props); + return ; } /** @public */ export function UserIcon(props: IconComponentProps) { - return useSystemIcon('user', props); + return ; } /** @public */ export function WarningIcon(props: IconComponentProps) { - return useSystemIcon('warning', props); + return ; } diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index fd28a59893..29bb0bcfa9 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -27,7 +27,6 @@ import { } from '@backstage/frontend-plugin-api'; import { - SystemIcon, compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; @@ -45,11 +44,12 @@ import { import { defaultDefinitionWidgets } from './components/ApiDefinitionCard'; import { rootRoute, registerComponentRouteRef } from './routes'; import { apiDocsConfigRef } from './config'; +import { AppIcon } from '@backstage/core-components'; const apiDocsNavItem = createNavItemExtension({ title: 'APIs', routeRef: convertLegacyRouteRef(rootRoute), - icon: () => , + icon: () => , }); const apiDocsConfigApi = createApiExtension({ diff --git a/yarn.lock b/yarn.lock index c8deda0b1d..06edaceef5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3818,7 +3818,6 @@ __metadata: "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-puppetdb": "workspace:^" "@backstage/plugin-stackstorm": "workspace:^" - "@backstage/test-utils": "workspace:^" "@backstage/version-bridge": "workspace:^" "@oriflame/backstage-plugin-score-card": ^0.8.0 "@testing-library/jest-dom": ^6.0.0 From bb48b3fd5e92fc913162cf881237e3ede3852f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Feb 2024 20:52:59 +0100 Subject: [PATCH 376/483] Update .changeset/unlucky-jobs-report.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Signed-off-by: Fredrik Adelöw --- .changeset/unlucky-jobs-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/unlucky-jobs-report.md b/.changeset/unlucky-jobs-report.md index 80e5ecbea0..4eab12d53e 100644 --- a/.changeset/unlucky-jobs-report.md +++ b/.changeset/unlucky-jobs-report.md @@ -4,4 +4,4 @@ Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md). -The `createRouter` function now has an optional `identity` argument, and instead gained the new `auth`, `httpAuth`, and `userInfo` arguments that should be set to the values of those respective `coreServices`. For users of the new backend system, this happens automatically without code changes. +The `createRouter` function now accepts `auth`, `httpAuth` and `userInfo` options. Theses are used internally to support the new backend system, and can be ignored. From 46138c2bd73eaa477f23954a0e79abb817232bb2 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 26 Feb 2024 15:21:35 -0500 Subject: [PATCH 377/483] update to `auth.provider.guest.*` login Signed-off-by: aramissennyeydd --- .changeset/cold-boats-sell.md | 17 ++++++++++-- docs/auth/guest/provider.md | 5 ++-- packages/backend-next/src/index.ts | 4 +-- .../templates/default-app/examples/org.yaml | 9 ------- .../config.d.ts | 27 ++++++++++++++----- .../src/module.ts | 2 +- .../src/resolvers.ts | 21 ++++++++++++--- 7 files changed, 57 insertions(+), 28 deletions(-) diff --git a/.changeset/cold-boats-sell.md b/.changeset/cold-boats-sell.md index af0dd1e295..5b0eba7301 100644 --- a/.changeset/cold-boats-sell.md +++ b/.changeset/cold-boats-sell.md @@ -2,9 +2,22 @@ '@backstage/plugin-auth-backend-module-guest-provider': minor --- -Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.guestEntityRef` config key) like so, +Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so, ```yaml title=app-config.yaml auth: - guestEntityRef: user:default/guest + providers: + guest: + userEntityRef: user:default/guest +``` + +This also adds a new property to control the ownership entity refs, + +```yaml title=app-config.yaml +auth: + providers: + guest: + ownershipEntityRefs: + - guests + - development/custom ``` diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index ca5bee1d09..c730877a47 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -59,9 +59,8 @@ Similar to the other authentication providers, you have to enable the provider i auth: providers: + guest: -+ development: - // new optional property to override the default value. -+ loginAs: user:default/guest ++ userEntityRef: user:default/guest ++ development: {} ``` We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 58fa994658..403b122ddf 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -20,6 +20,7 @@ const backend = createBackend(); 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-adr-backend')); backend.add(import('@backstage/plugin-app-backend/alpha')); @@ -57,7 +58,4 @@ backend.add(import('@backstage/plugin-sonarqube-backend')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); -backend.add(import('@backstage/plugin-auth-backend')); -backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); - backend.start(); diff --git a/packages/create-app/templates/default-app/examples/org.yaml b/packages/create-app/templates/default-app/examples/org.yaml index 1c4fb91a1e..a10e81fc7f 100644 --- a/packages/create-app/templates/default-app/examples/org.yaml +++ b/packages/create-app/templates/default-app/examples/org.yaml @@ -7,15 +7,6 @@ metadata: spec: memberOf: [guests] --- -# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user -apiVersion: backstage.io/v1alpha1 -kind: User -metadata: - name: guest - namespace: development -spec: - memberOf: [guests] ---- # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group apiVersion: backstage.io/v1alpha1 kind: Group diff --git a/plugins/auth-backend-module-guest-provider/config.d.ts b/plugins/auth-backend-module-guest-provider/config.d.ts index d6de29bed6..eb60492393 100644 --- a/plugins/auth-backend-module-guest-provider/config.d.ts +++ b/plugins/auth-backend-module-guest-provider/config.d.ts @@ -17,11 +17,26 @@ export interface Config { /** Configuration options for the auth plugin */ auth?: { - /** - * EXPERIMENTAL value: Allow users to configure what the guest provider logs in as. - * @visibility frontend - * @default user:default/guest - */ - guestEntityRef?: string; + providers: { + guest?: { + /** + * The entity reference to use for the guest user. + * @default user:development/guest + */ + userEntityRef?: string; + + /** + * A list of entity references to user for ownership of the guest user if the user + * is not found in the catalog. + * @default [userEntityRef] + */ + ownershipEntityRefs?: string[]; + + /** + * Allow users to sign in with the guest provider outside of their development environments. + */ + dangerouslyAllowOutsideDevelopment?: boolean; + }; + }; }; } diff --git a/plugins/auth-backend-module-guest-provider/src/module.ts b/plugins/auth-backend-module-guest-provider/src/module.ts index 75eb4f849c..7eac99b0a3 100644 --- a/plugins/auth-backend-module-guest-provider/src/module.ts +++ b/plugins/auth-backend-module-guest-provider/src/module.ts @@ -46,7 +46,7 @@ export const authModuleGuestProvider = createBackendModule({ factory: createProxyAuthProviderFactory({ authenticator: guestAuthenticator, signInResolver: signInAsGuestUser( - config.getOptionalString('auth.guestEntityRef'), + config.getConfig('auth.providers.guest'), ), }), }); diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts index b2c2f8cf7d..bec2ffe12c 100644 --- a/plugins/auth-backend-module-guest-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -15,7 +15,9 @@ */ import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; import { SignInResolver } from '@backstage/plugin-auth-node'; +import { NotImplementedError } from '@backstage/errors'; /** * Provide a default implementation of the user to resolve to. By default, this @@ -23,15 +25,26 @@ import { SignInResolver } from '@backstage/plugin-auth-node'; * catalog. If that user doesn't exist in the catalog, we will still create a * token for them so they can keep viewing. */ -export const signInAsGuestUser: (entityRef?: string) => SignInResolver<{}> = - (entityRef?: string) => async (_, ctx) => { +export const signInAsGuestUser: (config: Config) => SignInResolver<{}> = + (config: Config) => async (_, ctx) => { + if ( + process.env.NODE_ENV !== 'development' && + config.getOptionalBoolean('dangerouslyAllowOutsideDevelopment') !== true + ) { + throw new NotImplementedError( + 'The guest provider is NOT recommended for use outside of a development environment. If you want to enable this, set `auth.providers.guest.dangerouslyAllowOutsideDevelopment: true` in your app config.', + ); + } const userRef = - entityRef ?? + config.getOptionalString('userEntityRef') ?? stringifyEntityRef({ kind: 'user', namespace: 'development', name: 'guest', }); + const ownershipRefs = config.getOptionalStringArray( + 'ownershipEntityRefs', + ) ?? [userRef]; try { return ctx.signInWithCatalogUser({ entityRef: userRef }); } catch (err) { @@ -39,7 +52,7 @@ export const signInAsGuestUser: (entityRef?: string) => SignInResolver<{}> = return ctx.issueToken({ claims: { sub: userRef, - ent: [userRef], + ent: ownershipRefs, }, }); } From 5d1046dd206e1f264120a2ff28ef5acb89e8c3c3 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 26 Feb 2024 15:22:38 -0500 Subject: [PATCH 378/483] add config to guest provider Signed-off-by: aramissennyeydd --- plugins/auth-backend-module-guest-provider/package.json | 1 + plugins/auth-backend-module-guest-provider/src/resolvers.ts | 2 +- yarn.lock | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index edf3210759..bab85c2bd0 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -38,6 +38,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", "express": "^4.18.2" }, "files": [ diff --git a/plugins/auth-backend-module-guest-provider/src/resolvers.ts b/plugins/auth-backend-module-guest-provider/src/resolvers.ts index bec2ffe12c..35f724f746 100644 --- a/plugins/auth-backend-module-guest-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-guest-provider/src/resolvers.ts @@ -15,7 +15,7 @@ */ import { stringifyEntityRef } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; +import type { Config } from '@backstage/config'; import { SignInResolver } from '@backstage/plugin-auth-node'; import { NotImplementedError } from '@backstage/errors'; diff --git a/yarn.lock b/yarn.lock index 1bd1a644bb..3a1d1a2d65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4691,6 +4691,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" express: ^4.18.2 From 67276652e6404809c995e3e6c410b69c08d68080 Mon Sep 17 00:00:00 2001 From: Boris Bera Date: Sun, 25 Feb 2024 11:18:11 -0500 Subject: [PATCH 379/483] Make `spec.target` searchable in catalog table for location Signed-off-by: Boris Bera --- .changeset/wet-sheep-reply.md | 5 +++++ .../catalog/src/components/CatalogTable/columns.tsx | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .changeset/wet-sheep-reply.md diff --git a/.changeset/wet-sheep-reply.md b/.changeset/wet-sheep-reply.md new file mode 100644 index 0000000000..27635b133e --- /dev/null +++ b/.changeset/wet-sheep-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Allow the `spec.target` field to be searchable in the catalog table for locations. Previously, only the `spec.targets` field was be searchable. This makes locations generated by providers such as the `GithubEntityProvider` searchable in the catalog table. [#23098](https://github.com/backstage/backstage/issues/23098) diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 4260a0c26b..a191befe59 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -86,6 +86,18 @@ export const columnFactories = Object.freeze({ return { title: 'Targets', field: 'entity.spec.targets', + customFilterAndSearch: (query, row) => { + const targets = []; + if (Array.isArray(row.entity?.spec?.targets)) { + targets.push(...row.entity?.spec?.targets); + } else if (row.entity?.spec?.target) { + targets.push(row.entity?.spec?.target); + } + return targets + .join(', ') + .toLocaleUpperCase('en-US') + .includes(query.toLocaleUpperCase('en-US')); + }, render: ({ entity }) => ( <> {(entity?.spec?.targets || entity?.spec?.target) && ( From 1b2dc6c815c298bea792f3d9a1e0fd88d52a0578 Mon Sep 17 00:00:00 2001 From: Boris Bera Date: Mon, 26 Feb 2024 16:03:46 -0500 Subject: [PATCH 380/483] Appease typescript Signed-off-by: Boris Bera --- .../catalog/src/components/CatalogTable/columns.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index a191befe59..a955aaadb9 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -87,11 +87,14 @@ export const columnFactories = Object.freeze({ title: 'Targets', field: 'entity.spec.targets', customFilterAndSearch: (query, row) => { - const targets = []; - if (Array.isArray(row.entity?.spec?.targets)) { - targets.push(...row.entity?.spec?.targets); + let targets: JsonArray = []; + if ( + row.entity?.spec?.targets && + Array.isArray(row.entity?.spec?.targets) + ) { + targets = row.entity?.spec?.targets; } else if (row.entity?.spec?.target) { - targets.push(row.entity?.spec?.target); + targets = [row.entity?.spec?.target]; } return targets .join(', ') From 2e374918084a017595488dab1c2e37ad9bd38d9e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 01:44:23 +0100 Subject: [PATCH 381/483] Update beps/0003-auth-architecture-evolution/README.md Signed-off-by: Patrik Oldsberg --- beps/0003-auth-architecture-evolution/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index 998eb08dbd..e7bbb2804d 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -232,7 +232,7 @@ export default createBackendPlugin({ // Endpoint that sets the cookie for the user router.get('/cookie', async (req, res) => { - const { expiresAt } = await httpAuth.issueUserCookie(req); + const { expiresAt } = await httpAuth.issueUserCookie(res); res.json({ expiresAt: expiresAt.toISOString() }); }); From 41b65a5d0ce29c65401a7fc2095f78033ee43e5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 08:24:29 +0000 Subject: [PATCH 382/483] chore(deps): bump es5-ext from 0.10.62 to 0.10.63 Bumps [es5-ext](https://github.com/medikoo/es5-ext) from 0.10.62 to 0.10.63. - [Release notes](https://github.com/medikoo/es5-ext/releases) - [Changelog](https://github.com/medikoo/es5-ext/blob/main/CHANGELOG.md) - [Commits](https://github.com/medikoo/es5-ext/compare/v0.10.62...v0.10.63) --- updated-dependencies: - dependency-name: es5-ext dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 83f14f8ca5..593bd6f272 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26288,14 +26288,15 @@ __metadata: 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.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": - version: 0.10.62 - resolution: "es5-ext@npm:0.10.62" +"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" dependencies: es6-iterator: ^2.0.3 es6-symbol: ^3.1.3 + esniff: ^2.0.1 next-tick: ^1.1.0 - checksum: 25f42f6068cfc6e393cf670bc5bba249132c5f5ec2dd0ed6e200e6274aca2fed8e9aec8a31c76031744c78ca283c57f0b41c7e737804c6328c7b8d3fbcba7983 + checksum: 3bf04d9bac12a14e716a0a00b1706f538a3211da82703babd3e907deaeadaa30eab71202785027058d44d2a7c0e92e34631fb03fa63ef1097191e88de5223fda languageName: node linkType: hard @@ -26994,6 +26995,18 @@ __metadata: languageName: node linkType: hard +"esniff@npm:^2.0.1": + version: 2.0.1 + resolution: "esniff@npm:2.0.1" + dependencies: + d: ^1.0.1 + es5-ext: ^0.10.62 + event-emitter: ^0.3.5 + type: ^2.7.2 + checksum: d814c0e5c39bce9925b2e65b6d8767af72c9b54f35a65f9f3d6e8c606dce9aebe35a9599d30f15b0807743f88689f445163cfb577a425de4fb8c3c5bc16710cc + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" From 789986094e53c5b2856cd6fb8cd44673ac4c132f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 Feb 2024 09:57:34 +0100 Subject: [PATCH 383/483] fix(api-docs): wrap nav icon with compat wrapper Signed-off-by: Camila Belo --- plugins/api-docs/src/alpha.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 29bb0bcfa9..8cb43e9108 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -49,7 +49,7 @@ import { AppIcon } from '@backstage/core-components'; const apiDocsNavItem = createNavItemExtension({ title: 'APIs', routeRef: convertLegacyRouteRef(rootRoute), - icon: () => , + icon: () => compatWrapper(), }); const apiDocsConfigApi = createApiExtension({ From d42a5529292035003349eff2266fc44eed46c117 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 Feb 2024 10:00:53 +0100 Subject: [PATCH 384/483] fix: update core components changeset Signed-off-by: Camila Belo --- .changeset/friendly-news-sin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/friendly-news-sin.md b/.changeset/friendly-news-sin.md index 29843c54fb..af64190f9c 100644 --- a/.changeset/friendly-news-sin.md +++ b/.changeset/friendly-news-sin.md @@ -1,5 +1,5 @@ --- -'@backstage/core-components': minor +'@backstage/core-components': patch --- Create a component abstraction to consume system icons. From a959dc064df5e4b56cfe661b9bb21f808c6cbd33 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 27 Feb 2024 11:19:47 +0100 Subject: [PATCH 385/483] chore: fix build Signed-off-by: blam --- plugins/azure-sites-backend/src/plugin.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/azure-sites-backend/src/plugin.ts b/plugins/azure-sites-backend/src/plugin.ts index 90a9c416bd..8612158f54 100644 --- a/plugins/azure-sites-backend/src/plugin.ts +++ b/plugins/azure-sites-backend/src/plugin.ts @@ -36,9 +36,17 @@ export const azureSitesPlugin = createBackendPlugin({ logger: coreServices.logger, httpRouter: coreServices.httpRouter, permissions: coreServices.permissions, + discovery: coreServices.discovery, catalogApi: catalogServiceRef, }, - async init({ config, logger, httpRouter, permissions, catalogApi }) { + async init({ + config, + logger, + httpRouter, + permissions, + catalogApi, + discovery, + }) { const azureSitesApi = AzureSitesApi.fromConfig(config); httpRouter.use( await createRouter({ @@ -46,6 +54,7 @@ export const azureSitesPlugin = createBackendPlugin({ azureSitesApi, permissions, catalogApi, + discovery, }), ); }, From c7683174db07476338b74a8c2b8c1a675e020c8e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 12:13:38 +0100 Subject: [PATCH 386/483] azure-sites-backend: forward auth services in new system Signed-off-by: Patrik Oldsberg --- plugins/azure-sites-backend/src/plugin.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/azure-sites-backend/src/plugin.ts b/plugins/azure-sites-backend/src/plugin.ts index 8612158f54..a8afe4e430 100644 --- a/plugins/azure-sites-backend/src/plugin.ts +++ b/plugins/azure-sites-backend/src/plugin.ts @@ -37,6 +37,8 @@ export const azureSitesPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, permissions: coreServices.permissions, discovery: coreServices.discovery, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, catalogApi: catalogServiceRef, }, async init({ @@ -46,6 +48,8 @@ export const azureSitesPlugin = createBackendPlugin({ permissions, catalogApi, discovery, + auth, + httpAuth, }) { const azureSitesApi = AzureSitesApi.fromConfig(config); httpRouter.use( @@ -55,6 +59,8 @@ export const azureSitesPlugin = createBackendPlugin({ permissions, catalogApi, discovery, + auth, + httpAuth, }), ); }, From 13bb2ee787ef34eb30f16aa12c1607c94745bcd0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 09:48:58 +0100 Subject: [PATCH 387/483] backend-app-api: make sure auth service is compatible with existing plugins in dev Signed-off-by: Patrik Oldsberg --- .../auth/authServiceFactory.test.ts | 2 ++ .../auth/authServiceFactory.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts index f24264930a..4600110b5c 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts @@ -28,10 +28,12 @@ import { BackstageServicePrincipal, BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; +import { tokenManagerServiceFactory } from '../tokenManager'; // TODO: Ship discovery mock service in the service factory tester const mockDeps = [ discoveryServiceFactory(), + tokenManagerServiceFactory, mockServices.rootConfig.factory({ data: { backend: { diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 72d5635f61..19dfbe1299 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ServerTokenManager, TokenManager } from '@backstage/backend-common'; +import { TokenManager } from '@backstage/backend-common'; import { AuthService, BackstageCredentials, @@ -27,10 +27,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { AuthenticationError } from '@backstage/errors'; -import { - DefaultIdentityClient, - IdentityApiGetIdentityRequest, -} from '@backstage/plugin-auth-node'; +import { IdentityApiGetIdentityRequest } from '@backstage/plugin-auth-node'; import { decodeJwt } from 'jose'; /** @internal */ @@ -204,14 +201,15 @@ export const authServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.rootLogger, - discovery: coreServices.discovery, plugin: coreServices.pluginMetadata, + identity: coreServices.identity, + // Re-using the token manager makes sure that we use the same generated keys for + // development as plugins that have not yet been migrated. It's important that this + // keeps working as long as there are plugins that have not been migrated to the + // new auth services in the new backend system. + tokenManager: coreServices.tokenManager, }, - createRootContext({ config, logger }) { - return ServerTokenManager.fromConfig(config, { logger }); - }, - async factory({ discovery, config, plugin }, tokenManager) { - const identity = DefaultIdentityClient.create({ discovery }); + async factory({ config, plugin, identity, tokenManager }) { const disableDefaultAuthPolicy = Boolean( config.getOptionalBoolean( 'backend.auth.dangerouslyDisableDefaultAuthPolicy', From e1e540cd1da439edba15b697cc2d45e50293ca56 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 15 Feb 2024 18:40:18 +0100 Subject: [PATCH 388/483] kubernetes-backend: migrate to support new auth services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Carl-Erik Bergström Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/rare-dryers-check.md | 5 ++ packages/backend/src/plugins/kubernetes.ts | 1 + plugins/kubernetes-backend/api-report.md | 11 ++++ plugins/kubernetes-backend/src/plugin.ts | 17 ++++++- .../src/routes/resourceRoutes.test.ts | 51 +++++++++---------- .../src/routes/resourcesRoutes.ts | 21 ++++---- .../src/service/KubernetesBuilder.test.ts | 7 +-- .../src/service/KubernetesBuilder.ts | 28 +++++++++- 8 files changed, 98 insertions(+), 43 deletions(-) create mode 100644 .changeset/rare-dryers-check.md diff --git a/.changeset/rare-dryers-check.md b/.changeset/rare-dryers-check.md new file mode 100644 index 0000000000..584481f54d --- /dev/null +++ b/.changeset/rare-dryers-check.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +**BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 3bc6648862..5581083879 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -28,6 +28,7 @@ export default async function createPlugin( config: env.config, catalogApi, permissions: env.permissions, + discovery: env.discovery, }).build(); return router; } diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index afdb3bcf9a..60331ca044 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -5,12 +5,15 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; import { AuthMetadata as AuthMetadata_2 } from '@backstage/plugin-kubernetes-node'; +import { AuthService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { ClusterDetails as ClusterDetails_2 } from '@backstage/plugin-kubernetes-node'; import { Config } from '@backstage/config'; import { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; import express from 'express'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import * as k8sAuthTypes from '@backstage/plugin-kubernetes-node'; import { KubernetesClustersSupplier as KubernetesClustersSupplier_2 } from '@backstage/plugin-kubernetes-node'; import { KubernetesCredential as KubernetesCredential_2 } from '@backstage/plugin-kubernetes-node'; @@ -190,6 +193,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -272,11 +277,17 @@ export type KubernetesCredential = k8sAuthTypes.KubernetesCredential; // @public (undocumented) export interface KubernetesEnvironment { + // (undocumented) + auth?: AuthService; // (undocumented) catalogApi: CatalogApi; // (undocumented) config: Config; // (undocumented) + discovery: DiscoveryService; + // (undocumented) + httpAuth?: HttpAuthService; + // (undocumented) logger: Logger; // (undocumented) permissions: PermissionEvaluator; diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 74a8a68e53..d3aacd5190 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -178,10 +178,22 @@ export const kubernetesPlugin = createBackendPlugin({ http: coreServices.httpRouter, logger: coreServices.logger, config: coreServices.rootConfig, + discovery: coreServices.discovery, catalogApi: catalogServiceRef, permissions: coreServices.permissions, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, }, - async init({ http, logger, config, catalogApi, permissions }) { + async init({ + http, + logger, + config, + discovery, + catalogApi, + permissions, + auth, + httpAuth, + }) { const winstonLogger = loggerToWinstonLogger(logger); // TODO: expose all of the customization & extension points of the builder here const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ @@ -189,6 +201,9 @@ export const kubernetesPlugin = createBackendPlugin({ config, catalogApi, permissions, + discovery, + auth, + httpAuth, }) .setObjectsProvider(extPointObjectsProvider.getObjectsProvider()) .setClusterSupplier(extPointClusterSuplier.getClusterSupplier()) diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index d3dcc83bb0..9195551993 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -15,7 +15,11 @@ */ import request from 'supertest'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + mockCredentials, + mockServices, + startTestBackend, +} from '@backstage/backend-test-utils'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -102,12 +106,6 @@ describe('resourcesRoutes', () => { }, ], }, - backend: { - auth: { - // TODO: Remove once migrated to support new auth services - dangerouslyDisableDefaultAuthPolicy: true, - }, - }, }, }), import('@backstage/plugin-kubernetes-backend/alpha'), @@ -141,7 +139,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -168,7 +165,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -189,7 +185,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -214,7 +209,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -231,6 +225,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/workloads/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -239,7 +234,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: '', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -258,9 +256,12 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/workloads/query', @@ -279,7 +280,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', @@ -312,7 +312,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(200, { items: [ { @@ -340,7 +339,6 @@ describe('resourcesRoutes', () => { }, }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -365,7 +363,6 @@ describe('resourcesRoutes', () => { customResources: 'somestring', }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -390,7 +387,6 @@ describe('resourcesRoutes', () => { customResources: [], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -420,7 +416,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', message: 'entity is a required field' }, request: { @@ -448,7 +443,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -480,7 +474,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(400, { error: { name: 'InputError', @@ -497,6 +490,7 @@ describe('resourcesRoutes', () => { it('401 when no Auth header', async () => { await request(app) .post('/api/kubernetes/resources/custom/query') + .set('authorization', mockCredentials.none.header()) .send({ entityRef: 'component:someComponent', auth: { @@ -512,7 +506,10 @@ describe('resourcesRoutes', () => { }) .set('Content-Type', 'application/json') .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: '', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -538,9 +535,12 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'ffffff') + .set('Authorization', mockCredentials.user.invalidHeader()) .expect(401, { - error: { name: 'AuthenticationError', message: 'No Backstage token' }, + error: { + name: 'AuthenticationError', + message: 'User token is invalid', + }, request: { method: 'POST', url: '/api/kubernetes/resources/custom/query', @@ -566,7 +566,6 @@ describe('resourcesRoutes', () => { ], }) .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer Zm9vYmFy') .expect(500, { error: { name: 'Error', diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index 0468799908..6e05f69d04 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -19,15 +19,17 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; -import { InputError, AuthenticationError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; -import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; export const addResourceRoutesToRouter = ( router: express.Router, catalogApi: CatalogApi, objectsProvider: KubernetesObjectsProvider, + auth: AuthService, + httpAuth: HttpAuthService, ) => { const getEntityByReq = async (req: Request) => { const rawEntityRef = req.body.entityRef; @@ -44,23 +46,18 @@ export const addResourceRoutesToRouter = ( throw new InputError(`Invalid entity ref, ${error}`); } - const token = getBearerTokenFromAuthorizationHeader( - req.headers.authorization, - ); - - if (!token) { - throw new AuthenticationError('No Backstage token'); - } - - const entity = await catalogApi.getEntityByRef(entityRef, { - token: token, + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', }); + const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (!entity) { throw new InputError( `Entity ref missing, ${stringifyEntityRef(entityRef)}`, ); } + return entity; }; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 295d11570f..1ac653bb58 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -36,6 +36,7 @@ import { import { setupServer } from 'msw/node'; import { ServiceMock, + mockCredentials, mockServices, setupRequestMockHandlers, startTestBackend, @@ -757,9 +758,9 @@ metadata: }); it('serves permission integration endpoint', async () => { - const response = await request(app).get( - '/api/kubernetes/.well-known/backstage/permissions/metadata', - ); + const response = await request(app) + .get('/api/kubernetes/.well-known/backstage/permissions/metadata') + .set('authorization', mockCredentials.service.header()); expect(response.status).toEqual(200); expect(response.body).toMatchObject({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index fec4bc5cc0..c63d039ce9 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -64,6 +64,12 @@ import { } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesProxy } from './KubernetesProxy'; +import { createLegacyAuthAdapters } from '@backstage/backend-common'; +import { + AuthService, + DiscoveryService, + HttpAuthService, +} from '@backstage/backend-plugin-api'; /** * @@ -73,7 +79,10 @@ export interface KubernetesEnvironment { logger: Logger; config: Config; catalogApi: CatalogApi; + discovery: DiscoveryService; permissions: PermissionEvaluator; + auth?: AuthService; + httpAuth?: HttpAuthService; } /** @@ -131,6 +140,13 @@ export class KubernetesBuilder { router: Router(), } as unknown as KubernetesBuilderReturn; } + + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: this.env.auth, + httpAuth: this.env.httpAuth, + discovery: this.env.discovery, + }); + const customResources = this.buildCustomResources(); const fetcher = this.getFetcher(); @@ -158,6 +174,8 @@ export class KubernetesBuilder { this.env.catalogApi, proxy, permissions, + auth, + httpAuth, ); return { @@ -337,6 +355,8 @@ export class KubernetesBuilder { catalogApi: CatalogApi, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, + authService: AuthService, + httpAuth: HttpAuthService, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -391,7 +411,13 @@ export class KubernetesBuilder { }); }); - addResourceRoutesToRouter(router, catalogApi, objectsProvider); + addResourceRoutesToRouter( + router, + catalogApi, + objectsProvider, + authService, + httpAuth, + ); return router; } From d50a02bf7d4c1c3c019a407e6462c1416b6375de Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 13:43:00 +0100 Subject: [PATCH 389/483] Introducing createMockActionContext for scaffolder Signed-off-by: bnechyporenko --- packages/scaffolder-test-utils/.eslintrc.js | 1 + packages/scaffolder-test-utils/CHANGELOG.md | 1 + packages/scaffolder-test-utils/README.md | 12 +++++ packages/scaffolder-test-utils/api-report.md | 18 +++++++ .../scaffolder-test-utils/catalog-info.yaml | 9 ++++ packages/scaffolder-test-utils/knip-report.md | 2 + packages/scaffolder-test-utils/package.json | 48 +++++++++++++++++++ .../src/actions/index.ts | 17 +++++++ .../src/actions/mockActionConext.ts | 46 ++++++++++++++++++ packages/scaffolder-test-utils/src/index.ts | 17 +++++++ .../package.json | 3 +- .../src/actions/azure.test.ts | 17 ++----- yarn.lock | 18 +++++++ 13 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 packages/scaffolder-test-utils/.eslintrc.js create mode 100644 packages/scaffolder-test-utils/CHANGELOG.md create mode 100644 packages/scaffolder-test-utils/README.md create mode 100644 packages/scaffolder-test-utils/api-report.md create mode 100644 packages/scaffolder-test-utils/catalog-info.yaml create mode 100644 packages/scaffolder-test-utils/knip-report.md create mode 100644 packages/scaffolder-test-utils/package.json create mode 100644 packages/scaffolder-test-utils/src/actions/index.ts create mode 100644 packages/scaffolder-test-utils/src/actions/mockActionConext.ts create mode 100644 packages/scaffolder-test-utils/src/index.ts diff --git a/packages/scaffolder-test-utils/.eslintrc.js b/packages/scaffolder-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/scaffolder-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/scaffolder-test-utils/CHANGELOG.md b/packages/scaffolder-test-utils/CHANGELOG.md new file mode 100644 index 0000000000..e290a56ced --- /dev/null +++ b/packages/scaffolder-test-utils/CHANGELOG.md @@ -0,0 +1 @@ +# @backstage/scaffolder-test-utils diff --git a/packages/scaffolder-test-utils/README.md b/packages/scaffolder-test-utils/README.md new file mode 100644 index 0000000000..e5810058b3 --- /dev/null +++ b/packages/scaffolder-test-utils/README.md @@ -0,0 +1,12 @@ +# @backstage/scaffolder-test-utils + +Contains utilities that can be used when testing scaffolder features. + +## Installation + +Install the package via Yarn into your own packages: + +```sh +cd # if within a monorepo +yarn add --dev @backstage/scaffolder-test-utils +``` diff --git a/packages/scaffolder-test-utils/api-report.md b/packages/scaffolder-test-utils/api-report.md new file mode 100644 index 0000000000..b95b020f1c --- /dev/null +++ b/packages/scaffolder-test-utils/api-report.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/scaffolder-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; + +// @public +export const createMockActionContext: < + TActionInput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = JsonObject, +>( + input?: TActionInput | undefined, +) => ActionContext; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/scaffolder-test-utils/catalog-info.yaml b/packages/scaffolder-test-utils/catalog-info.yaml new file mode 100644 index 0000000000..596e9b1f64 --- /dev/null +++ b/packages/scaffolder-test-utils/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-scaffolder-test-utils + title: '@backstage/scaffolder-test-utils' +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/scaffolder-test-utils/knip-report.md b/packages/scaffolder-test-utils/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/packages/scaffolder-test-utils/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/packages/scaffolder-test-utils/package.json b/packages/scaffolder-test-utils/package.json new file mode 100644 index 0000000000..9db983d828 --- /dev/null +++ b/packages/scaffolder-test-utils/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/scaffolder-test-utils", + "version": "0.0.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/scaffolder-test-utils" + }, + "backstage": { + "role": "node-library" + }, + "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" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@types/react": "*" + }, + "files": [ + "dist" + ], + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@backstage/types": "workspace:^" + }, + "peerDependencies": { + "@types/jest": "*" + } +} diff --git a/packages/scaffolder-test-utils/src/actions/index.ts b/packages/scaffolder-test-utils/src/actions/index.ts new file mode 100644 index 0000000000..161bae8521 --- /dev/null +++ b/packages/scaffolder-test-utils/src/actions/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { createMockActionContext } from './mockActionConext'; diff --git a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts new file mode 100644 index 0000000000..a838c091f8 --- /dev/null +++ b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts @@ -0,0 +1,46 @@ +/* + * 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 { PassThrough } from 'stream'; +import { getVoidLogger } from '@backstage/backend-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { JsonObject } from '@backstage/types'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; + +/** + * A utility method to create a mock action context for scaffolder actions. + * + * @param input - a schema for user input parameters + * + * @public + */ +export const createMockActionContext = < + TActionInput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = JsonObject, +>( + input?: TActionInput, +): ActionContext => { + const mockDir = createMockDirectory(); + + return { + workspacePath: mockDir.path, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + input: (input ? input : {}) as TActionInput, + }; +}; diff --git a/packages/scaffolder-test-utils/src/index.ts b/packages/scaffolder-test-utils/src/index.ts new file mode 100644 index 0000000000..19eae8d569 --- /dev/null +++ b/packages/scaffolder-test-utils/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 './actions'; diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 26be04f108..2bbcedf897 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -47,7 +47,8 @@ "yaml": "^2.0.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts index 4401e01007..0dbacaf699 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts @@ -34,10 +34,9 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { WebApi } from 'azure-devops-node-api'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:azure', () => { const config = new ConfigReader({ @@ -54,16 +53,10 @@ describe('publish:azure', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishAzureAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org', - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + + const mockContext = createMockActionContext({ + repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org', + }); const mockGitClient = { createRepository: jest.fn(), diff --git a/yarn.lock b/yarn.lock index 3b404940a9..f6a737a69f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8209,6 +8209,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" azure-devops-node-api: ^12.0.0 yaml: ^2.0.0 languageName: unknown @@ -9864,6 +9865,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/scaffolder-test-utils@workspace:^, @backstage/scaffolder-test-utils@workspace:packages/scaffolder-test-utils": + version: 0.0.0-use.local + resolution: "@backstage/scaffolder-test-utils@workspace:packages/scaffolder-test-utils" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/types": "workspace:^" + "@testing-library/jest-dom": ^6.0.0 + "@types/react": "*" + peerDependencies: + "@types/jest": "*" + languageName: unknown + linkType: soft + "@backstage/test-utils@workspace:^, @backstage/test-utils@workspace:packages/test-utils": version: 0.0.0-use.local resolution: "@backstage/test-utils@workspace:packages/test-utils" From 1615cfdf3f5ee61dcdf9420962397e272ffba970 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 14:07:16 +0100 Subject: [PATCH 390/483] wip Signed-off-by: bnechyporenko --- .../src/actions/mockActionConext.ts | 13 +++++++--- .../src/actions/azure.examples.test.ts | 11 ++------ .../package.json | 1 + .../src/actions/bitbucketCloud.test.ts | 18 ++++--------- ...itbucketCloudPipelinesRun.examples.test.ts | 12 ++------- .../bitbucketCloudPipelinesRun.test.ts | 12 ++------- .../package.json | 1 + .../src/actions/bitbucketServer.test.ts | 18 ++++--------- .../bitbucketServerPullRequest.test.ts | 26 +++++++------------ .../package.json | 1 + .../src/actions/bitbucket.examples.test.ts | 18 ++++--------- .../src/actions/bitbucket.test.ts | 18 ++++--------- .../package.json | 1 + .../confluenceToMarkdown.examples.test.ts | 11 +++----- yarn.lock | 5 ++++ 15 files changed, 57 insertions(+), 109 deletions(-) diff --git a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts index a838c091f8..6b2e992487 100644 --- a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts +++ b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts @@ -19,12 +19,15 @@ import { getVoidLogger } from '@backstage/backend-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import * as winston from 'winston'; /** * A utility method to create a mock action context for scaffolder actions. * * @param input - a schema for user input parameters * + * @param workspacePath + * @param logger * @public */ export const createMockActionContext = < @@ -32,12 +35,14 @@ export const createMockActionContext = < TActionOutput extends JsonObject = JsonObject, >( input?: TActionInput, + workspacePath?: string, + logger?: winston.Logger, ): ActionContext => { - const mockDir = createMockDirectory(); - return { - workspacePath: mockDir.path, - logger: getVoidLogger(), + workspacePath: workspacePath + ? workspacePath + : createMockDirectory().resolve('workspace'), + logger: logger ? logger : getVoidLogger(), logStream: new PassThrough(), output: jest.fn(), createTemporaryDirectory: jest.fn(), diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 27989650ce..7634480bc5 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -18,11 +18,10 @@ import yaml from 'yaml'; import { ConfigReader } from '@backstage/config'; import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; -import { getVoidLogger } from '@backstage/backend-common'; import { WebApi } from 'azure-devops-node-api'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), @@ -55,13 +54,7 @@ describe('publish:azure examples', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishAzureAction({ integrations, config }); - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const mockGitClient = { createRepository: jest.fn(), diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 1a40b517d6..1ec9c4083c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 7045d58e01..0b7bf95a33 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -32,9 +32,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:bitbucketCloud', () => { const config = new ConfigReader({ @@ -50,17 +49,10 @@ describe('publish:bitbucketCloud', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketCloudAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, - }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 90ae2c08b9..1786ff26e2 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ @@ -39,14 +38,7 @@ describe('bitbucket:pipelines:run', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createBitbucketPipelinesRunAction({ integrations }); - const mockContext = { - input: {}, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const responseJson = { repository: { links: { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index dc76a79898..64b4e8f7fe 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { PassThrough } from 'stream'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ @@ -37,14 +36,7 @@ describe('bitbucket:pipelines:run', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createBitbucketPipelinesRunAction({ integrations }); - const mockContext = { - input: {}, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); const workspace = 'test-workspace'; const repo_slug = 'test-repo-slug'; const responseJson = { diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 8f4841180d..dee14897ab 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index ab7b52996f..9a51fef5e8 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -32,9 +32,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:bitbucketServer', () => { const config = new ConfigReader({ @@ -60,17 +59,10 @@ describe('publish:bitbucketServer', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketServerAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - repoVisibility: 'private' as const, - }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'private' as const, + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index 3af47853c3..c4ae8b9dde 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -32,8 +32,7 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:bitbucketServer:pull-request', () => { const config = new ConfigReader({ @@ -62,21 +61,14 @@ describe('publish:bitbucketServer:pull-request', () => { integrations, config, }); - const mockContext = { - input: { - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - title: 'Add Scaffolder actions for Bitbucket Server', - description: - 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Server', - targetBranch: 'master', - sourceBranch: 'develop', - }, - workspacePath: 'wsp', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + title: 'Add Scaffolder actions for Bitbucket Server', + description: + 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Server', + targetBranch: 'master', + sourceBranch: 'develop', + }); const responseOfBranches = { size: 3, limit: 25, diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index c34c07c008..2a88ff8525 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 73325ab5bf..b89508e1ca 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -32,12 +32,11 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; import { sep } from 'path'; import { examples } from './bitbucket.examples'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ @@ -61,17 +60,10 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index d6e068c039..80279afec6 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -31,9 +31,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ @@ -57,17 +56,10 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = { - input: { - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 07d72d73a6..67b899991e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index f3bf1fef35..bdbd341660 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; import { getVoidLogger } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; @@ -28,6 +27,7 @@ import { setupServer } from 'msw/node'; import { examples } from './confluenceToMarkdown.examples'; import yaml from 'yaml'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; @@ -71,14 +71,11 @@ describe('confluence:transform:markdown examples', () => { }), search: jest.fn(), }; - mockContext = { - input: yaml.parse(examples[0].example).steps[0].input, + mockContext = createMockActionContext( + yaml.parse(examples[0].example).steps[0].input, workspacePath, logger, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + ); mockDir.setContent({ 'workspace/mkdocs.yml': 'File contents' }); }); diff --git a/yarn.lock b/yarn.lock index f6a737a69f..6bbb18da35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8227,6 +8227,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8246,6 +8247,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8267,6 +8269,7 @@ __metadata: "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^" "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -8286,6 +8289,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" fs-extra: ^11.2.0 git-url-parse: ^14.0.0 msw: ^1.0.0 @@ -9877,6 +9881,7 @@ __metadata: "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^6.0.0 "@types/react": "*" + winston: ^3.2.1 peerDependencies: "@types/jest": "*" languageName: unknown From c94a2f946c578570d7a2e167b46b336db6fdd6b1 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sun, 18 Feb 2024 15:02:15 +0100 Subject: [PATCH 391/483] wip Signed-off-by: bnechyporenko --- packages/scaffolder-test-utils/package.json | 4 +- .../src/actions/mockActionConext.ts | 46 +++++--- .../src/actions/azure.test.ts | 2 +- .../src/actions/bitbucketCloud.test.ts | 6 +- .../bitbucketServerPullRequest.test.ts | 14 ++- .../src/actions/bitbucket.examples.test.ts | 6 +- .../src/actions/bitbucket.test.ts | 6 +- .../confluenceToMarkdown.examples.test.ts | 6 +- .../confluence/confluenceToMarkdown.test.ts | 10 +- .../package.json | 1 + .../src/actions/fetch/cookiecutter.test.ts | 21 +--- .../package.json | 1 + .../src/actions/gerrit.test.ts | 12 +- .../src/actions/gerritReview.test.ts | 12 +- .../package.json | 1 + .../src/actions/gitea.test.ts | 12 +- .../package.json | 1 + .../src/actions/github.examples.test.ts | 12 +- .../src/actions/github.test.ts | 12 +- .../githubActionsDispatch.examples.test.ts | 12 +- .../src/actions/githubActionsDispatch.test.ts | 12 +- .../actions/githubAutolinks.examples.test.ts | 16 +-- .../src/actions/githubAutolinks.test.ts | 50 ++++----- .../actions/githubDeployKey.examples.test.ts | 11 +- .../src/actions/githubDeployKey.test.ts | 12 +- .../githubEnvironment.examples.test.ts | 11 +- .../src/actions/githubEnvironment.test.ts | 12 +- .../githubIssuesLabel.examples.test.ts | 11 +- .../src/actions/githubIssuesLabel.test.ts | 12 +- .../githubPullRequest.examples.test.ts | 11 +- .../src/actions/githubPullRequest.test.ts | 103 +++--------------- .../actions/githubRepoCreate.examples.test.ts | 12 +- .../src/actions/githubRepoCreate.test.ts | 12 +- .../actions/githubRepoPush.examples.test.ts | 11 +- .../src/actions/githubRepoPush.test.ts | 12 +- .../actions/githubWebhook.examples.test.ts | 11 +- .../src/actions/githubWebhook.test.ts | 12 +- yarn.lock | 5 + 38 files changed, 173 insertions(+), 360 deletions(-) diff --git a/packages/scaffolder-test-utils/package.json b/packages/scaffolder-test-utils/package.json index 9db983d828..0dbb2e5943 100644 --- a/packages/scaffolder-test-utils/package.json +++ b/packages/scaffolder-test-utils/package.json @@ -38,9 +38,11 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/test-utils": "workspace:^", - "@backstage/types": "workspace:^" + "@backstage/types": "workspace:^", + "winston": "^3.2.1" }, "peerDependencies": { "@types/jest": "*" diff --git a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts index 6b2e992487..b9c79b68bf 100644 --- a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts +++ b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts @@ -20,32 +20,50 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; import * as winston from 'winston'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; /** * A utility method to create a mock action context for scaffolder actions. * - * @param input - a schema for user input parameters * - * @param workspacePath - * @param logger + * * @public + * @param options */ export const createMockActionContext = < TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, ->( - input?: TActionInput, - workspacePath?: string, - logger?: winston.Logger, -): ActionContext => { - return { - workspacePath: workspacePath - ? workspacePath - : createMockDirectory().resolve('workspace'), - logger: logger ? logger : getVoidLogger(), +>(options?: { + input?: TActionInput; + workspacePath?: string; + logger?: winston.Logger; + templateInfo?: TemplateInfo; +}): ActionContext => { + const defaultContext = { + logger: getVoidLogger(), logStream: new PassThrough(), output: jest.fn(), createTemporaryDirectory: jest.fn(), - input: (input ? input : {}) as TActionInput, + input: {} as TActionInput, + }; + + const createDefaultWorkspace = () => ({ + workspacePath: createMockDirectory().resolve('workspace'), + }); + + if (!options) { + return { + ...defaultContext, + ...createDefaultWorkspace(), + }; + } + + const { input, workspacePath, logger, templateInfo } = options; + return { + ...defaultContext, + ...(workspacePath ? { workspacePath } : createDefaultWorkspace()), + ...(logger && { logger }), + ...(input && { input }), + templateInfo, }; }; diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts index 0dbacaf699..6b076b57d7 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts @@ -55,7 +55,7 @@ describe('publish:azure', () => { const action = createPublishAzureAction({ integrations, config }); const mockContext = createMockActionContext({ - repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org', + input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, }); const mockGitClient = { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 0b7bf95a33..bc56bde073 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -50,8 +50,10 @@ describe('publish:bitbucketCloud', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketCloudAction({ integrations, config }); const mockContext = createMockActionContext({ - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }, }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index c4ae8b9dde..00d6db384a 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -62,12 +62,14 @@ describe('publish:bitbucketServer:pull-request', () => { config, }); const mockContext = createMockActionContext({ - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - title: 'Add Scaffolder actions for Bitbucket Server', - description: - 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Server', - targetBranch: 'master', - sourceBranch: 'develop', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + title: 'Add Scaffolder actions for Bitbucket Server', + description: + 'I just made a Pull Request that Add Scaffolder actions for Bitbucket Server', + targetBranch: 'master', + sourceBranch: 'develop', + }, }); const responseOfBranches = { size: 3, diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index b89508e1ca..17ac8ff522 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -61,8 +61,10 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); const mockContext = createMockActionContext({ - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }, }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index 80279afec6..0a3ddd8c9d 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -57,8 +57,10 @@ describe('publish:bitbucket', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketAction({ integrations, config }); const mockContext = createMockActionContext({ - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }, }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index bdbd341660..47befca7ac 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -71,11 +71,11 @@ describe('confluence:transform:markdown examples', () => { }), search: jest.fn(), }; - mockContext = createMockActionContext( - yaml.parse(examples[0].example).steps[0].input, + mockContext = createMockActionContext({ + input: yaml.parse(examples[0].example).steps[0].input, workspacePath, logger, - ); + }); mockDir.setContent({ 'workspace/mkdocs.yml': 'File contents' }); }); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 39c9c98544..889d59c8bf 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; + import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; import { getVoidLogger } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; @@ -26,6 +26,7 @@ import { import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; @@ -69,7 +70,7 @@ describe('confluence:transform:markdown', () => { }), search: jest.fn(), }; - mockContext = { + mockContext = createMockActionContext({ input: { confluenceUrls: [ 'https://nodomain.confluence.com/display/testing/mkdocs', @@ -79,10 +80,7 @@ describe('confluence:transform:markdown', () => { }, workspacePath, logger, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockDir.setContent({ 'workspace/mkdocs.yml': 'File contents' }); }); diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 236ebd6c88..a512c2ea36 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0" }, diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index af63ca0e48..abf94c9e0a 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -14,19 +14,15 @@ * limitations under the License. */ -import { - getVoidLogger, - UrlReader, - ContainerRunner, -} from '@backstage/backend-common'; +import { UrlReader, ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); @@ -88,7 +84,7 @@ describe('fetch:cookiecutter', () => { beforeEach(() => { jest.resetAllMocks(); - mockContext = { + mockContext = createMockActionContext({ input: { url: 'https://google.com/cookie/cutter', targetPath: 'something', @@ -96,16 +92,7 @@ describe('fetch:cookiecutter', () => { help: 'me', }, }, - templateInfo: { - entityRef: 'template:default/cookiecutter', - baseUrl: 'somebase', - }, - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + }); mockDir.setContent({ template: {} }); commandExists.mockResolvedValue(null); diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index dad19dc06b..36f7c613a7 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -49,6 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 2bd7783cb6..20c8b74de6 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -33,9 +33,8 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:gerrit', () => { const config = new ConfigReader({ @@ -53,18 +52,13 @@ describe('publish:gerrit', () => { const description = 'for the lols'; const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGerritAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo', description, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts index eeae3871ee..cee4b8344c 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts @@ -24,9 +24,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishGerritReviewAction } from './gerritReview'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { commitAndPushRepo } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('publish:gerrit:review', () => { const config = new ConfigReader({ @@ -43,18 +42,13 @@ describe('publish:gerrit:review', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGerritReviewAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo', gitCommitMessage: 'Review from backstage', }, - workspacePath: 'workspace', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index bc4912d76c..2b1b4b6176 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -49,6 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 5e1e94b1b4..a3f7d80e88 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { setupServer } from 'msw/node'; jest.mock('@backstage/plugin-scaffolder-node', () => { @@ -48,17 +47,12 @@ describe('publish:gitea', () => { const description = 'for the lols'; const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGiteaAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitea.com?repo=repo&owner=owner', description, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 58651b3c07..8e1a05134f 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@types/libsodium-wrappers": "^0.7.10", "fs-extra": "^11.2.0", "jest-when": "^3.1.0", diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts index 7ea5e025ce..61b5787d46 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts @@ -36,14 +36,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createPublishGithubAction } from './github'; import { examples } from './github.examples'; import yaml from 'yaml'; @@ -101,19 +100,14 @@ describe('publish:github', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { initRepoAndPushMocked.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index 75d9b74a8b..cdf2fb552a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -34,15 +34,14 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { }); import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; import { when } from 'jest-when'; -import { PassThrough } from 'stream'; import { createPublishGithubAction } from './github'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { @@ -103,19 +102,14 @@ describe('publish:github', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { initRepoAndPushMocked.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts index fe04142a98..9496fdd1bb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts @@ -20,10 +20,9 @@ import { GithubCredentialsProvider, } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import yaml from 'yaml'; import { examples } from './githubActionsDispatch.examples'; @@ -56,18 +55,13 @@ describe('github:actions:dispatch', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', workflowId: 'a-workflow-id', branchOrTagName: 'main', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts index 3d59e43695..e69a92a0a2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -20,9 +20,8 @@ import { GithubCredentialsProvider, } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; const mockOctokit = { @@ -54,18 +53,13 @@ describe('github:actions:dispatch', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', workflowId: 'a-workflow-id', branchOrTagName: 'main', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts index 612f68dea2..2df283bf44 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, @@ -22,8 +21,8 @@ import { ScmIntegrations, } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubAutolinksAction } from './githubAutolinks'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { examples } from './githubAutolinks.examples'; import yaml from 'yaml'; @@ -70,14 +69,11 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }); + await action.handler( + createMockActionContext({ + input, + }), + ); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts index 95c0226104..0a529e0377 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { createGithubAutolinksAction } from './githubAutolinks'; const mockOctokit = { @@ -53,13 +53,7 @@ describe('github:autolinks:create', () => { const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const workspacePath = createMockDirectory().resolve('workspace'); it('should call the githubApis for creating alphanumeric autolink reference', async () => { githubCredentialsProvider = @@ -74,14 +68,16 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input: { - repoUrl: 'github.com?repo=repo&owner=owner', - keyPrefix: 'TICKET-', - urlTemplate: 'https://example.com/TICKET?query=', - }, - ...mockContext, - }); + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + }, + workspacePath, + }), + ); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', @@ -104,15 +100,17 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler({ - input: { - repoUrl: 'github.com?repo=repo&owner=owner', - keyPrefix: 'TICKET-', - urlTemplate: 'https://example.com/TICKET?query=', - isAlphanumeric: false, - }, - ...mockContext, - }); + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + isAlphanumeric: false, + }, + workspacePath, + }), + ); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts index c8e2a5f102..1fb3facb83 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts @@ -14,11 +14,10 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createGithubDeployKeyAction } from './githubDeployKey'; import yaml from 'yaml'; import { examples } from './githubDeployKey.examples'; -import { PassThrough } from 'stream'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -55,13 +54,7 @@ describe('Usage examples', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts index 4df9294ff5..cb8f84e563 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubDeployKeyAction } from './githubDeployKey'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -55,19 +54,14 @@ describe('github:deployKey:create', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', publicKey: 'pubkey', privateKey: 'privkey', deployKeyName: 'Push Tags', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts index 9d5dbfd35c..1130f41a68 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubEnvironmentAction } from './githubEnvironment'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -59,13 +58,7 @@ describe('github:environment:create examples', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { mockOctokit.rest.actions.getEnvironmentPublicKey.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index 8ec3d84e36..a590257a2e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGithubEnvironmentAction } from './githubEnvironment'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -58,17 +57,12 @@ describe('github:environment:create', () => { const integrations = ScmIntegrations.fromConfig(config); let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', name: 'envname', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { mockOctokit.rest.actions.getEnvironmentPublicKey.mockResolvedValue({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 9d74d53c6d..5afee9e9ed 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -15,14 +15,13 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubIssuesLabelAction } from './githubIssuesLabel'; import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; @@ -64,13 +63,7 @@ describe('github:issues:label examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 3da16e677a..72200f0ea0 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -20,10 +20,9 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; import { getOctokitOptions } from './helpers'; jest.mock('./helpers', () => { @@ -62,18 +61,13 @@ describe('github:issues:label', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', number: '1', labels: ['label1', 'label2'], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts index 7e5d107026..083eb78066 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts @@ -15,14 +15,13 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import yaml from 'yaml'; import { examples } from './githubPullRequest.examples'; @@ -57,13 +56,7 @@ describe('publish:github:pull-request examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); let fakeClient: { createPullRequest: jest.Mock; rest: { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index 3fe4f62e04..5cbaac6de6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GithubCredentialsProvider, @@ -25,9 +25,9 @@ import { TemplateAction, } from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; -import { Writable } from 'stream'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -131,14 +131,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -196,14 +189,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -263,14 +249,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request with only relevant files', async () => { @@ -322,14 +301,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -382,14 +354,7 @@ describe('createPublishGithubPullRequestAction', () => { mockDir.setContent({ [workspacePath]: {} }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request and requests a review from the given reviewers', async () => { @@ -434,14 +399,7 @@ describe('createPublishGithubPullRequestAction', () => { mockDir.setContent({ [workspacePath]: {} }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('does not call the API endpoint for requesting reviewers', async () => { @@ -470,14 +428,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -526,14 +477,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -592,14 +536,7 @@ describe('createPublishGithubPullRequestAction', () => { }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { await instance.handler(ctx); @@ -653,14 +590,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { @@ -705,14 +635,7 @@ describe('createPublishGithubPullRequestAction', () => { [workspacePath]: { 'file.txt': 'Hello there!' }, }); - ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + ctx = createMockActionContext({ input, workspacePath }); }); it('creates a pull request', async () => { 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 2377e9a7eb..3dd8a44e07 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 @@ -23,14 +23,13 @@ jest.mock('./gitHelpers', () => { }; }); -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createGithubRepoCreateAction } from './githubRepoCreate'; import { entityRefToName } from './gitHelpers'; import yaml from 'yaml'; @@ -82,16 +81,11 @@ describe('github:repo:create examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { githubCredentialsProvider = 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 c72a969e96..ce29de7a1e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -15,6 +15,7 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; jest.mock('./gitHelpers', () => { return { @@ -23,7 +24,6 @@ jest.mock('./gitHelpers', () => { }; }); -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, @@ -31,7 +31,6 @@ import { ScmIntegrations, } from '@backstage/integration'; import { when } from 'jest-when'; -import { PassThrough } from 'stream'; import { createGithubRepoCreateAction } from './githubRepoCreate'; import { entityRefToName } from './gitHelpers'; @@ -82,19 +81,14 @@ describe('github:repo:create', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { githubCredentialsProvider = diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts index 8f8c9bf8af..8c48811eb1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts @@ -29,14 +29,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubRepoPushAction } from './githubRepoPush'; import { examples } from './githubRepoPush.examples'; import yaml from 'yaml'; @@ -102,13 +101,7 @@ describe('github:repo:push examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts index e16da0981d..31b3a7c95e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts @@ -59,14 +59,13 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers'; import { createGithubRepoPushAction } from './githubRepoPush'; @@ -103,19 +102,14 @@ describe('github:repo:push', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repository&owner=owner', description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts index ce1fe76c53..17dd371a21 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts @@ -14,14 +14,13 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createGithubWebhookAction } from './githubWebhook'; import yaml from 'yaml'; import { examples } from './githubWebhook.examples'; @@ -56,13 +55,7 @@ describe('github:webhook examples', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts index 7901210a6e..8c5a57542f 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts @@ -20,10 +20,9 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PassThrough } from 'stream'; const mockOctokit = { rest: { @@ -66,17 +65,12 @@ describe('github:repository:webhook:create', () => { }); }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'github.com?repo=repo&owner=owner', webhookUrl: 'https://example.com/payload', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); it('should call the githubApi for creating repository Webhook', async () => { const repoUrl = 'github.com?repo=repo&owner=owner'; diff --git a/yarn.lock b/yarn.lock index 6bbb18da35..c8edc34ae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8311,6 +8311,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^11.0.0 @@ -8333,6 +8334,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 @@ -8351,6 +8353,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" msw: ^1.0.0 node-fetch: ^2.6.7 yaml: ^2.0.0 @@ -8369,6 +8372,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@octokit/webhooks": ^10.0.0 "@types/libsodium-wrappers": ^0.7.10 fs-extra: ^11.2.0 @@ -9876,6 +9880,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" From 85a9bba49d6771f0ca9c09c78c996a41c96b26e0 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Mon, 19 Feb 2024 20:55:09 +0100 Subject: [PATCH 392/483] wip Signed-off-by: bnechyporenko --- packages/scaffolder-test-utils/api-report.md | 17 +- .../src/actions/mockActionConext.ts | 21 ++- .../src/actions/bitbucketServer.test.ts | 6 +- .../package.json | 1 + ...reateGitlabGroupEnsureExistsAction.test.ts | 11 +- .../actions/createGitlabIssueAction.test.ts | 27 +--- ...bProjectAccessTokenAction.examples.test.ts | 12 +- ...eateGitlabProjectDeployTokenAction.test.ts | 12 +- .../src/actions/gitlab.examples.test.ts | 12 +- .../src/actions/gitlab.test.ts | 40 ++--- .../src/actions/gitlabMergeRequest.test.ts | 148 +++--------------- .../src/actions/gitlabRepoPush.test.ts | 76 ++------- .../package.json | 1 + .../src/actions/fetch/rails/index.test.ts | 25 +-- .../package.json | 1 + .../src/actions/createProject.test.ts | 25 ++- .../package.json | 1 + .../src/actions/run/yeoman.test.ts | 11 +- plugins/scaffolder-backend/package.json | 1 + .../builtin/catalog/fetch.examples.test.ts | 13 +- .../actions/builtin/catalog/fetch.test.ts | 14 +- .../builtin/catalog/register.examples.test.ts | 12 +- .../actions/builtin/catalog/register.test.ts | 13 +- .../builtin/catalog/write.examples.test.ts | 14 +- .../actions/builtin/catalog/write.test.ts | 15 +- .../builtin/debug/log.examples.test.ts | 25 +-- .../actions/builtin/debug/log.test.ts | 12 +- .../builtin/debug/wait.examples.test.ts | 16 +- .../actions/builtin/debug/wait.test.ts | 16 +- .../builtin/fetch/plain.examples.test.ts | 23 ++- .../actions/builtin/fetch/plain.test.ts | 13 +- .../builtin/fetch/plainFile.examples.test.ts | 14 +- .../actions/builtin/fetch/plainFile.test.ts | 13 +- .../builtin/fetch/template.examples.test.ts | 34 ++-- .../actions/builtin/fetch/template.test.ts | 45 ++---- .../filesystem/delete.examples.test.ts | 11 +- .../actions/builtin/filesystem/delete.test.ts | 11 +- .../filesystem/rename.examples.test.ts | 11 +- .../actions/builtin/filesystem/rename.test.ts | 11 +- yarn.lock | 5 + 40 files changed, 218 insertions(+), 571 deletions(-) diff --git a/packages/scaffolder-test-utils/api-report.md b/packages/scaffolder-test-utils/api-report.md index b95b020f1c..4e50a55f5e 100644 --- a/packages/scaffolder-test-utils/api-report.md +++ b/packages/scaffolder-test-utils/api-report.md @@ -3,15 +3,30 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; +import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import * as winston from 'winston'; +import { Writable } from 'stream'; // @public export const createMockActionContext: < TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, >( - input?: TActionInput | undefined, + options?: + | { + input?: TActionInput | undefined; + logger?: winston.Logger | undefined; + logStream?: Writable | undefined; + secrets?: TaskSecrets | undefined; + templateInfo?: TemplateInfo | undefined; + workspacePath?: string | undefined; + } + | undefined, ) => ActionContext; // (No @packageDocumentation comment for this package) diff --git a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts index b9c79b68bf..1b9f6200f9 100644 --- a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts +++ b/packages/scaffolder-test-utils/src/actions/mockActionConext.ts @@ -14,30 +14,30 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; +import { PassThrough, Writable } from 'stream'; import { getVoidLogger } from '@backstage/backend-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; -import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { ActionContext, TaskSecrets } from '@backstage/plugin-scaffolder-node'; import * as winston from 'winston'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; /** * A utility method to create a mock action context for scaffolder actions. * - * - * * @public - * @param options + * @param options - optional parameters to override default mock context */ export const createMockActionContext = < TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, >(options?: { input?: TActionInput; - workspacePath?: string; logger?: winston.Logger; + logStream?: Writable; + secrets?: TaskSecrets; templateInfo?: TemplateInfo; + workspacePath?: string; }): ActionContext => { const defaultContext = { logger: getVoidLogger(), @@ -58,12 +58,19 @@ export const createMockActionContext = < }; } - const { input, workspacePath, logger, templateInfo } = options; + const { input, logger, logStream, secrets, templateInfo, workspacePath } = + options; + return { ...defaultContext, ...(workspacePath ? { workspacePath } : createDefaultWorkspace()), + ...(workspacePath && { + createTemporaryDirectory: jest.fn().mockResolvedValue(workspacePath), + }), ...(logger && { logger }), + ...(logStream && { logStream }), ...(input && { input }), + ...(secrets && { secrets }), templateInfo, }; }; diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index 9a51fef5e8..1c1ec09368 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -60,8 +60,10 @@ describe('publish:bitbucketServer', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishBitbucketServerAction({ integrations, config }); const mockContext = createMockActionContext({ - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - repoVisibility: 'private' as const, + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'private' as const, + }, }); const server = setupServer(); setupRequestMockHandlers(server); diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index dc96a0f356..72514db885 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -57,6 +57,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "jest-date-mock": "^1.0.8" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts index 72dcddc83d..a94a844d4c 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; @@ -35,13 +34,7 @@ jest.mock('@gitbeaker/node', () => ({ })); describe('gitlab:group:ensureExists', () => { - const mockContext = { - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); afterEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index fe9e556656..d33cbb37dc 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createGitlabIssueAction, IssueType } from './createGitlabIssueAction'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; @@ -60,18 +59,14 @@ describe('gitlab:issues:create', () => { const action = createGitlabIssueAction({ integrations }); it('should return a Gitlab issue when called with minimal input params', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, title: 'Computer banks to rule the world', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, @@ -109,7 +104,7 @@ describe('gitlab:issues:create', () => { }); it('should return a Gitlab issue when called with oAuth Token', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, @@ -117,11 +112,7 @@ describe('gitlab:issues:create', () => { token: 'myAwesomeToken', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, @@ -159,7 +150,7 @@ describe('gitlab:issues:create', () => { }); it('should return a Gitlab issue when called with several input params', async () => { - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: 123, @@ -173,11 +164,7 @@ describe('gitlab:issues:create', () => { labels: 'operation:mindcrime', }, workspacePath: 'seen2much', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); mockGitlabClient.Issues.create.mockResolvedValue({ id: 42, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts index ab4fe0e637..90f4fca283 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; import yaml from 'yaml'; import { createGitlabProjectAccessTokenAction } from './createGitlabProjectAccessTokenAction'; // Adjust the import based on your project structure import { examples } from './createGitlabProjectAccessTokenAction.examples'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { DateTime } from 'luxon'; @@ -59,16 +58,11 @@ describe('gitlab:projectAccessToken:create examples', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createGitlabProjectAccessTokenAction({ integrations }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts index 1d3ae804c1..c626a20124 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts @@ -15,10 +15,9 @@ */ import { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; const mockGitlabClient = { ProjectDeployTokens: { @@ -52,7 +51,7 @@ describe('gitlab:create-deploy-token', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createGitlabProjectDeployTokenAction({ integrations }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', projectId: '123', @@ -60,12 +59,7 @@ describe('gitlab:create-deploy-token', () => { username: 'tokenuser', scopes: ['read_repository'], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts index e5aa610ebc..3bd6b0e5ff 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import yaml from 'yaml'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => { return { @@ -31,8 +32,6 @@ import { createPublishGitlabAction } from './gitlab'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { examples } from './gitlab.examples'; const mockGitlabClient = { @@ -76,16 +75,11 @@ describe('publish:gitlab', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGitlabAction({ integrations, config }); - const mockContext = { + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts index 5aca0a73e5..40712966ba 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts @@ -29,9 +29,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishGitlabAction } from './gitlab'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; const mockGitlabClient = { Namespaces: { @@ -83,7 +82,8 @@ describe('publish:gitlab', () => { const integrations = ScmIntegrations.fromConfig(config); const action = createPublishGitlabAction({ integrations, config }); - const mockContext = { + + const mockContext = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -91,13 +91,8 @@ describe('publish:gitlab', () => { ci_config_path: '.gitlab-ci.yml', }, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithSettings = { + }); + const mockContextWithSettings = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -108,13 +103,8 @@ describe('publish:gitlab', () => { topics: ['topic1', 'topic2'], }, }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithBranches = { + }); + const mockContextWithBranches = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -135,13 +125,8 @@ describe('publish:gitlab', () => { }, ], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const mockContextWithVariables = { + }); + const mockContextWithVariables = createMockActionContext({ input: { repoUrl: 'gitlab.com?repo=repo&owner=owner', repoVisibility: 'private' as const, @@ -155,12 +140,7 @@ describe('publish:gitlab', () => { }, ], }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index 0883a5e9f0..4f6585e7b3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Writable } from 'stream'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -118,14 +118,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Projects.show).not.toHaveBeenCalled(); @@ -160,14 +153,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith('owner/repo'); @@ -205,14 +191,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -240,14 +219,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -281,14 +253,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -321,14 +286,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -361,14 +319,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -400,14 +351,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( @@ -435,14 +379,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -484,14 +421,7 @@ describe('createGitLabMergeRequest', () => { irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -528,14 +458,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -570,14 +493,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -612,14 +528,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( @@ -657,14 +566,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -702,14 +604,7 @@ describe('createGitLabMergeRequest', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -739,14 +634,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await expect(instance.handler(ctx)).rejects.toThrow( 'Relative path is not allowed to refer to a directory outside its parent', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts index 840f506540..24ba3abcd3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRootLogger, getRootLogger } from '@backstage/backend-common'; +import { createRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Writable } from 'stream'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createGitlabRepoPushAction } from './gitlabRepoPush'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); @@ -93,14 +93,7 @@ describe('createGitLabCommit', () => { 'foo.txt': 'Hello there!', }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -139,14 +132,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -183,14 +169,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -227,14 +206,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledTimes(0); @@ -276,14 +248,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -325,14 +290,7 @@ describe('createGitLabCommit', () => { }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); @@ -366,14 +324,7 @@ describe('createGitLabCommit', () => { commitAction: 'create', }; - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await expect(instance.handler(ctx)).rejects.toThrow( 'Relative path is not allowed to refer to a directory outside its parent', @@ -398,14 +349,7 @@ describe('createGitLabCommit', () => { 'foo.txt': 'Hello there!', }, }); - const ctx = { - createTemporaryDirectory: jest.fn(), - output: jest.fn(), - logger: getRootLogger(), - logStream: new Writable(), - input, - workspacePath, - }; + const ctx = createMockActionContext({ input, workspacePath }); await instance.handler(ctx); expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index f67f3fbb22..5426049996 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0", "@types/node": "^18.17.8", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 7548ddd21d..0f56bd55ec 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -27,18 +27,14 @@ jest.mock('./railsNewRunner', () => { }; }); -import { - ContainerRunner, - getVoidLogger, - UrlReader, -} from '@backstage/backend-common'; +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'path'; -import { PassThrough } from 'stream'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); @@ -53,8 +49,7 @@ describe('fetch:rails', () => { }), ); - const mockTmpDir = mockDir.path; - const mockContext = { + const mockContext = createMockActionContext({ input: { url: 'https://rubyonrails.org/generator', targetPath: 'something', @@ -66,12 +61,8 @@ describe('fetch:rails', () => { baseUrl: 'somebase', entityRef: 'template:default/myTemplate', }, - workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + workspacePath: mockDir.path, + }); const mockReader: UrlReader = { readUrl: jest.fn(), @@ -102,7 +93,7 @@ describe('fetch:rails', () => { expect(fetchContents).toHaveBeenCalledWith({ reader: mockReader, integrations, - baseUrl: mockContext.templateInfo.baseUrl, + baseUrl: mockContext.templateInfo?.baseUrl, fetchUrl: mockContext.input.url, outputPath: resolvePath(mockContext.workspacePath), }); @@ -112,7 +103,7 @@ describe('fetch:rails', () => { await action.handler(mockContext); expect(mockRailsTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, + workspacePath: mockContext.workspacePath, logStream: mockContext.logStream, values: mockContext.input.values, }); @@ -128,7 +119,7 @@ describe('fetch:rails', () => { }); expect(mockRailsTemplater.run).toHaveBeenCalledWith({ - workspacePath: mockTmpDir, + workspacePath: mockContext.workspacePath, logStream: mockContext.logStream, values: { ...mockContext.input.values, diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index f1aab87354..fb942617f3 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -46,6 +46,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@backstage/types": "workspace:^", "msw": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 972b0f1e8e..715f21d944 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -15,6 +15,7 @@ */ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; @@ -42,19 +43,17 @@ describe('sentry:project:create action', () => { name: string; slug?: string; authToken?: string; - }> => ({ - workspacePath: './dev/proj', - createTemporaryDirectory: jest.fn(), - logger: jest.createMockFromModule('winston'), - logStream: jest.createMockFromModule('stream'), - input: { - organizationSlug: 'org', - teamSlug: 'team', - name: 'test project', - authToken: randomBytes(5).toString('hex'), - }, - output: jest.fn(), - }); + }> => + createMockActionContext({ + workspacePath: './dev/proj', + logger: jest.createMockFromModule('winston'), + input: { + organizationSlug: 'org', + teamSlug: 'team', + name: 'test project', + authToken: randomBytes(5).toString('hex'), + }, + }); it('should request sentry project create with specified parameters.', async () => { expect.assertions(3); diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 2ea82dd5d2..9abfe95ac0 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -39,6 +39,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index 146c29f4ac..ae18a25365 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -18,9 +18,8 @@ import { yeomanRun } from './yeomanRun'; jest.mock('./yeomanRun'); -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import os from 'os'; -import { PassThrough } from 'stream'; import { createRunYeomanAction } from './yeoman'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; @@ -46,18 +45,14 @@ describe('run:yeoman', () => { const options = { code: 'owner', }; - mockContext = { + mockContext = createMockActionContext({ input: { namespace, args, options, }, workspacePath: mockTmpDir, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), - }; + }); await action.handler(mockContext); expect(yeomanRun).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 4fec5bdfaf..5884450a89 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -95,6 +95,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/scaffolder-test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts index 161ff9d1de..62e0cef0dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; @@ -36,14 +34,9 @@ describe('catalog:fetch examples', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), + const mockContext = createMockActionContext({ secrets: { backstageToken: 'secret' }, - }; + }); beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index bed15f2e39..43d7660a9a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; @@ -34,14 +32,10 @@ describe('catalog:fetch', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), + const mockContext = createMockActionContext({ secrets: { backstageToken: 'secret' }, - }; + }); + beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts index eb5aa88c0f..e9900221e3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -44,13 +42,7 @@ describe('catalog:register', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts index bae9f04575..b035a8c94d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -42,13 +40,8 @@ describe('catalog:register', () => { catalogClient: catalogClient as unknown as CatalogApi, }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); + beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts index daa8f4f6b1..6f410db07e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts @@ -20,24 +20,16 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; -import { PassThrough } from 'stream'; -import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; import * as yaml from 'yaml'; import { examples } from './write.examples'; +import os from 'os'; describe('catalog:write', () => { const action = createCatalogWriteAction(); - - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ workspacePath: os.tmpdir() }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts index dd6f29f99b..1b06c930f9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts @@ -20,9 +20,8 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; -import { PassThrough } from 'stream'; import os from 'os'; -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; @@ -31,18 +30,14 @@ import * as yaml from 'yaml'; describe('catalog:write', () => { const action = createCatalogWriteAction(); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - beforeEach(() => { jest.resetAllMocks(); }); + const mockContext = createMockActionContext({ + workspacePath: os.tmpdir(), + }); + it('should write the catalog-info.yml in the workspace', async () => { const entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts index 901f3e5011..de008433f0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; @@ -30,15 +30,10 @@ describe('debug:log examples', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + workspacePath, + }); const action = createDebugLogAction(); @@ -51,12 +46,10 @@ describe('debug:log examples', () => { }); it('should log message', async () => { - const context = { + await action.handler({ ...mockContext, input: yaml.parse(examples[0].example).steps[0].input, - }; - - await action.handler(context); + }); expect(logStream.write).toHaveBeenCalledTimes(1); expect(logStream.write).toHaveBeenCalledWith( @@ -65,12 +58,10 @@ describe('debug:log examples', () => { }); it('should log the workspace content, if active', async () => { - const context = { + await action.handler({ ...mockContext, input: yaml.parse(examples[1].example).steps[0].input, - }; - - await action.handler(context); + }); expect(logStream.write).toHaveBeenCalledTimes(1); expect(logStream.write).toHaveBeenCalledWith( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index d6fc5174b3..c9bd0f8cbe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; @@ -29,15 +29,7 @@ describe('debug:log', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), - logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext({ workspacePath, logStream }); const action = createDebugLogAction(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 9b755d86dd..00574dedfd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; import { examples } from './wait.examples'; import yaml from 'yaml'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('debug:wait examples', () => { const action = createWaitAction(); @@ -28,18 +27,9 @@ describe('debug:wait examples', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 6f80604a6d..1424ca3012 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); @@ -26,18 +25,9 @@ describe('debug:wait', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - - const mockContext = { - input: {}, - baseUrl: 'somebase', - workspacePath, - logger: getVoidLogger(), + const mockContext = createMockActionContext({ logStream, - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts index 6b308a97eb..7ce945a08f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts @@ -16,14 +16,13 @@ import yaml from 'yaml'; -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainAction } from './plain'; -import { PassThrough } from 'stream'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { examples } from './plain.examples'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ @@ -50,19 +49,15 @@ describe('fetch:plain examples', () => { }); const action = createFetchPlainAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should fetch plain', async () => { - await action.handler({ - ...mockContext, - input: yaml.parse(examples[0].example).steps[0].input, - }); + await action.handler( + createMockActionContext({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }), + ); expect(fetchContents).toHaveBeenCalledWith( expect.objectContaining({ outputPath: resolvePath(mockContext.workspacePath), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 917624acf4..7468779f3a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -19,14 +19,13 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainAction } from './plain'; -import { PassThrough } from 'stream'; describe('fetch:plain', () => { const integrations = ScmIntegrations.fromConfig( @@ -47,13 +46,7 @@ describe('fetch:plain', () => { }); const action = createFetchPlainAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should disallow a target path outside working directory', async () => { await expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts index 2ee17c29fe..25993caf96 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts @@ -14,19 +14,19 @@ * limitations under the License. */ +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; + jest.mock('@backstage/plugin-scaffolder-node', () => { const actual = jest.requireActual('@backstage/plugin-scaffolder-node'); return { ...actual, fetchFile: jest.fn() }; }); import yaml from 'yaml'; -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainFileAction } from './plainFile'; -import { PassThrough } from 'stream'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { examples } from './plainFile.examples'; @@ -49,13 +49,7 @@ describe('fetch:plain:file examples', () => { }); const action = createFetchPlainFileAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should fetch plain', async () => { await action.handler({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts index 7ea74a809b..8f889bef4b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts @@ -19,14 +19,13 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchFile: jest.fn() }; }); -import os from 'os'; import { resolve as resolvePath } from 'path'; -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainFileAction } from './plainFile'; -import { PassThrough } from 'stream'; describe('fetch:plain:file', () => { const integrations = ScmIntegrations.fromConfig( @@ -47,13 +46,7 @@ describe('fetch:plain:file', () => { }); const action = createFetchPlainFileAction({ integrations, reader }); - const mockContext = { - workspacePath: os.tmpdir(), - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + const mockContext = createMockActionContext(); it('should disallow a target path outside working directory', async () => { await expect( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index a1c8381178..29a267ceab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -16,13 +16,9 @@ import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { - getVoidLogger, - resolvePackagePath, - UrlReader, -} from '@backstage/backend-common'; +import { resolvePackagePath, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { createFetchTemplateAction } from './template'; import { ActionContext, @@ -61,23 +57,15 @@ describe('fetch:template examples', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const logger = getVoidLogger(); - - const mockContext = (input: any) => ({ - templateInfo: { - baseUrl: 'base-url', - entityRef: 'template:default/test-template', - }, - input: input, - output: jest.fn(), - logStream: new PassThrough(), - logger, - workspacePath, - - async createTemporaryDirectory() { - return fs.mkdtemp(mockDir.resolve('tmp-')); - }, - }); + const mockContext = (input: any) => + createMockActionContext({ + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', + }, + input, + workspacePath, + }); beforeEach(() => { mockDir.clear(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 25b65462e6..3cd8f61e77 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -21,13 +21,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { - getVoidLogger, - resolvePackagePath, - UrlReader, -} from '@backstage/backend-common'; +import { resolvePackagePath, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { PassThrough } from 'stream'; import { createFetchTemplateAction } from './template'; import { fetchContents, @@ -35,6 +30,7 @@ import { TemplateAction, } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; type FetchTemplateInput = ReturnType< typeof createFetchTemplateAction @@ -59,29 +55,22 @@ describe('fetch:template', () => { const mockDir = createMockDirectory(); const workspacePath = mockDir.resolve('workspace'); - const logger = getVoidLogger(); - - const mockContext = (inputPatch: Partial = {}) => ({ - templateInfo: { - baseUrl: 'base-url', - entityRef: 'template:default/test-template', - }, - input: { - url: './skeleton', - targetPath: './target', - values: { - test: 'value', + const mockContext = (inputPatch: Partial = {}) => + createMockActionContext({ + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', }, - ...inputPatch, - }, - output: jest.fn(), - logStream: new PassThrough(), - logger, - workspacePath, - async createTemporaryDirectory() { - return fs.mkdtemp(mockDir.resolve('tmp-')); - }, - }); + input: { + url: './skeleton', + targetPath: './target', + values: { + test: 'value', + }, + ...inputPatch, + }, + workspacePath, + }); beforeEach(() => { mockDir.setContent({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts index 023f049ebc..8611486dac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -15,8 +15,7 @@ */ import { createFilesystemDeleteAction } from './delete'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import { resolve as resolvePath } from 'path'; import fs from 'fs-extra'; import yaml from 'yaml'; @@ -31,16 +30,12 @@ describe('fs:delete examples', () => { const files: string[] = yaml.parse(examples[0].example).steps[0].input.files; - const mockContext = { + const mockContext = createMockActionContext({ input: { files: files, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index 2a4f45b862..2cddb9f5a6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemDeleteAction } from './delete'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -27,16 +26,12 @@ describe('fs:delete', () => { const mockDir = createMockDirectory(); const workspacePath = resolvePath(mockDir.path, 'workspace'); - const mockContext = { + const mockContext = createMockActionContext({ input: { files: ['unit-test-a.js', 'unit-test-b.js'], }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts index cbb0fa0d0b..5e9ba84465 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './rename.examples'; @@ -31,16 +30,12 @@ describe('fs:rename examples', () => { const mockDir = createMockDirectory(); const workspacePath = resolvePath(mockDir.path, 'workspace'); - const mockContext = { + const mockContext = createMockActionContext({ input: { files: files, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index d37e967f43..b081200b71 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -16,8 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -41,16 +40,12 @@ describe('fs:rename', () => { to: 'brand-new-folder', }, ]; - const mockContext = { + const mockContext = createMockActionContext({ input: { files: mockInputFiles, }, workspacePath, - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; + }); beforeEach(() => { jest.restoreAllMocks(); diff --git a/yarn.lock b/yarn.lock index c8edc34ae3..2d9d22d1ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8399,6 +8399,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@gitbeaker/core": ^35.8.0 "@gitbeaker/node": ^35.8.0 "@gitbeaker/rest": ^39.25.0 @@ -8421,6 +8422,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^11.0.0 @@ -8441,6 +8443,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 yaml: ^2.3.3 @@ -8455,6 +8458,7 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@backstage/types": "workspace:^" winston: ^3.2.1 yeoman-environment: ^3.9.1 @@ -8490,6 +8494,7 @@ __metadata: "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/scaffolder-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/fs-extra": ^11.0.0 From f44589ddeccb3d12404b5e38ba85c1a0d7afa34b Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Mon, 19 Feb 2024 20:59:08 +0100 Subject: [PATCH 393/483] wip Signed-off-by: bnechyporenko --- .changeset/kind-pants-speak.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .changeset/kind-pants-speak.md diff --git a/.changeset/kind-pants-speak.md b/.changeset/kind-pants-speak.md new file mode 100644 index 0000000000..be600c44d2 --- /dev/null +++ b/.changeset/kind-pants-speak.md @@ -0,0 +1,23 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-github': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-backend-module-azure': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/scaffolder-test-utils': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-app-api': patch +'@backstage/backend-common': patch +--- + +Introduced createMockActionContext to unify the way of creating scaffolder mock context. +It will help to maintain tests in a long run during structural changes of action context. From ab123770de83792d355d8696b4478f64c82c8163 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Mon, 19 Feb 2024 21:08:16 +0100 Subject: [PATCH 394/483] Updated changeset Signed-off-by: bnechyporenko --- .changeset/kind-pants-speak.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.changeset/kind-pants-speak.md b/.changeset/kind-pants-speak.md index be600c44d2..8af9a7e890 100644 --- a/.changeset/kind-pants-speak.md +++ b/.changeset/kind-pants-speak.md @@ -12,11 +12,8 @@ '@backstage/plugin-scaffolder-backend-module-azure': patch '@backstage/plugin-scaffolder-backend-module-gitea': patch '@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-catalog-backend-module-azure': patch '@backstage/scaffolder-test-utils': patch '@backstage/plugin-scaffolder-backend': patch -'@backstage/backend-app-api': patch -'@backstage/backend-common': patch --- Introduced createMockActionContext to unify the way of creating scaffolder mock context. From d1cd9605e94a7fb601d9a295b9fefff02d9a3d46 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 20 Feb 2024 08:40:55 +0100 Subject: [PATCH 395/483] wip Signed-off-by: bnechyporenko --- .../src/actions/fetch/cookiecutter.test.ts | 5 +++++ .../src/actions/githubAutolinks.examples.test.ts | 11 +++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index abf94c9e0a..80d7681e94 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -92,6 +92,11 @@ describe('fetch:cookiecutter', () => { help: 'me', }, }, + templateInfo: { + entityRef: 'template:default/cookiecutter', + baseUrl: 'somebase', + }, + workspacePath: mockTmpDir, }); mockDir.setContent({ template: {} }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts index 2df283bf44..c62cee8c00 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts @@ -54,9 +54,12 @@ describe('github:autolinks:create', () => { const integrations = ScmIntegrations.fromConfig(config); let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; + const input = yaml.parse(examples[0].example).steps[0].input; + const mockContext = createMockActionContext({ + input, + }); it('should call the githubApis for creating autolink reference', async () => { - const input = yaml.parse(examples[0].example).steps[0].input; githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); action = createGithubAutolinksAction({ @@ -69,11 +72,7 @@ describe('github:autolinks:create', () => { id: '1', }, }); - await action.handler( - createMockActionContext({ - input, - }), - ); + await action.handler(mockContext); expect(mockOctokit.rest.repos.createAutolink).toHaveBeenCalledWith({ owner: 'owner', From b3e6c7777740fe2019bf756c9ef17270a4dd7d82 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 20 Feb 2024 09:13:15 +0100 Subject: [PATCH 396/483] wip Signed-off-by: bnechyporenko --- packages/scaffolder-test-utils/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/scaffolder-test-utils/package.json b/packages/scaffolder-test-utils/package.json index 0dbb2e5943..604eff388f 100644 --- a/packages/scaffolder-test-utils/package.json +++ b/packages/scaffolder-test-utils/package.json @@ -40,7 +40,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@backstage/test-utils": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1" }, diff --git a/yarn.lock b/yarn.lock index 2d9d22d1ec..341f32e976 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9887,7 +9887,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^6.0.0 "@types/react": "*" From 813d6dbbb2605160881a94a9400aaa4b9e182035 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Tue, 20 Feb 2024 09:36:51 +0100 Subject: [PATCH 397/483] wip Signed-off-by: bnechyporenko --- packages/scaffolder-test-utils/package.json | 3 --- yarn.lock | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/scaffolder-test-utils/package.json b/packages/scaffolder-test-utils/package.json index 604eff388f..b1aaad001e 100644 --- a/packages/scaffolder-test-utils/package.json +++ b/packages/scaffolder-test-utils/package.json @@ -42,8 +42,5 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1" - }, - "peerDependencies": { - "@types/jest": "*" } } diff --git a/yarn.lock b/yarn.lock index 341f32e976..aa9ab30dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9891,8 +9891,6 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@types/react": "*" winston: ^3.2.1 - peerDependencies: - "@types/jest": "*" languageName: unknown linkType: soft From 04585753738b8468c5afb3364921d3adbf763da1 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Fri, 23 Feb 2024 21:56:11 +0100 Subject: [PATCH 398/483] wip Signed-off-by: bnechyporenko --- .../writing-tests-for-actions.md | 57 +++++++ .../src/actions/github.ts | 152 ++++++++++-------- 2 files changed, 142 insertions(+), 67 deletions(-) create mode 100644 docs/features/software-templates/writing-tests-for-actions.md diff --git a/docs/features/software-templates/writing-tests-for-actions.md b/docs/features/software-templates/writing-tests-for-actions.md new file mode 100644 index 0000000000..7a75250479 --- /dev/null +++ b/docs/features/software-templates/writing-tests-for-actions.md @@ -0,0 +1,57 @@ +--- +id: writing-tests-for-actions +title: Writing Tests For Actions +description: How to write tests for actions +--- + +Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with +Unit tests to be sure that your actions do what they suppose to do. + +Make sure that you cover the most of scenario's, which could happen with the action. +One of indispensable part of the test is to supply the context to a handler of action for the execution. +We encourage you to use a utility method for that, so your tests are immune to structural changes of context. +What is inevitably going to happen during the time. + +Example how to use it: + +```typescript +import { createMockActionContext } from '@backstage/scaffolder-test-utils'; + +const mockContext = createMockActionContext({ + input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, +}); + +await action.handler(mockContext); + +expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://dev.azure.com/organization/project/_git/repo', +); +``` + +One thing to be aware about: if you would like to call `createMockActionContext` inside `it`, +you have to provide a `workspacePath`. By default, `createMockActionContext` uses +`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. +This implementation contains a hook inside which creates this limitation. So in this case you can do then: + +```typescript +describe('github:autolinks:create', async () => { + const workspacePath = createMockDirectory().resolve('workspace'); + // ... + + it('should call the githubApis for creating alphanumeric autolink reference', async () => { + // ... + await action.handler( + createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + keyPrefix: 'TICKET-', + urlTemplate: 'https://example.com/TICKET?query=', + }, + workspacePath, + }), + ); + //... + }); +}); +``` diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index f32b65bc9a..3a1beae8e8 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -213,79 +213,97 @@ export function createPublishGithubAction(options: { requiredCommitSigning = false, } = ctx.input; - const octokitOptions = await getOctokitOptions({ - integrations, - credentialsProvider: githubCredentialsProvider, - token: providedToken, - repoUrl: repoUrl, - }); - const client = new Octokit(octokitOptions); + const { _commitHash, _remoteUrl, _repoContentsUrl } = + await ctx.checkpoint?.( + 'repo.create', + async (): Promise<{ + _commitHash: string; + _remoteUrl: string; + _repoContentsUrl: string; + }> => { + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } + if (!owner) { + throw new InputError( + 'Invalid repository owner provided in repoUrl', + ); + } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( - client, - repo, - owner, - repoVisibility, - description, - homepage, - deleteBranchOnMerge, - allowMergeCommit, - allowSquashMerge, - squashMergeCommitTitle, - squashMergeCommitMessage, - allowRebaseMerge, - allowAutoMerge, - access, - collaborators, - hasProjects, - hasWiki, - hasIssues, - topics, - repoVariables, - secrets, - oidcCustomization, - ctx.logger, - ); + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + homepage, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + squashMergeCommitTitle, + squashMergeCommitMessage, + allowRebaseMerge, + allowAutoMerge, + access, + collaborators, + hasProjects, + hasWiki, + hasIssues, + topics, + repoVariables, + secrets, + oidcCustomization, + ctx.logger, + ); - const remoteUrl = newRepo.clone_url; - const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; + const remoteUrl = newRepo.clone_url; + const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; - const commitResult = await initRepoPushAndProtect( - remoteUrl, - octokitOptions.auth, - ctx.workspacePath, - ctx.input.sourcePath, - defaultBranch, - protectDefaultBranch, - protectEnforceAdmins, - owner, - client, - repo, - requireCodeOwnerReviews, - bypassPullRequestAllowances, - requiredApprovingReviewCount, - restrictions, - requiredStatusCheckContexts, - requireBranchesToBeUpToDate, - requiredConversationResolution, - config, - ctx.logger, - gitCommitMessage, - gitAuthorName, - gitAuthorEmail, - dismissStaleReviews, - requiredCommitSigning, - ); + const commitResult = await initRepoPushAndProtect( + remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, + defaultBranch, + protectDefaultBranch, + protectEnforceAdmins, + owner, + client, + repo, + requireCodeOwnerReviews, + bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, + requiredStatusCheckContexts, + requireBranchesToBeUpToDate, + requiredConversationResolution, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + dismissStaleReviews, + requiredCommitSigning, + ); - ctx.output('commitHash', commitResult?.commitHash); - ctx.output('remoteUrl', remoteUrl); - ctx.output('repoContentsUrl', repoContentsUrl); + return { + _commitHash: commitResult?.commitHash, + _remoteUrl: remoteUrl, + _repoContentsUrl: repoContentsUrl, + }; + }, + )!!; + + ctx.output('commitHash', _commitHash); + ctx.output('remoteUrl', _remoteUrl); + ctx.output('repoContentsUrl', _repoContentsUrl); }, }); } From 6766c4e7438b13644bcf21ed6be19470887e7cd0 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Sat, 24 Feb 2024 10:55:12 +0100 Subject: [PATCH 399/483] wip Signed-off-by: bnechyporenko --- .../src/actions/github.ts | 152 ++++++++---------- 1 file changed, 67 insertions(+), 85 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 3a1beae8e8..f32b65bc9a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -213,97 +213,79 @@ export function createPublishGithubAction(options: { requiredCommitSigning = false, } = ctx.input; - const { _commitHash, _remoteUrl, _repoContentsUrl } = - await ctx.checkpoint?.( - 'repo.create', - async (): Promise<{ - _commitHash: string; - _remoteUrl: string; - _repoContentsUrl: string; - }> => { - const octokitOptions = await getOctokitOptions({ - integrations, - credentialsProvider: githubCredentialsProvider, - token: providedToken, - repoUrl: repoUrl, - }); - const client = new Octokit(octokitOptions); + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { - throw new InputError( - 'Invalid repository owner provided in repoUrl', - ); - } + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( - client, - repo, - owner, - repoVisibility, - description, - homepage, - deleteBranchOnMerge, - allowMergeCommit, - allowSquashMerge, - squashMergeCommitTitle, - squashMergeCommitMessage, - allowRebaseMerge, - allowAutoMerge, - access, - collaborators, - hasProjects, - hasWiki, - hasIssues, - topics, - repoVariables, - secrets, - oidcCustomization, - ctx.logger, - ); + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + homepage, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + squashMergeCommitTitle, + squashMergeCommitMessage, + allowRebaseMerge, + allowAutoMerge, + access, + collaborators, + hasProjects, + hasWiki, + hasIssues, + topics, + repoVariables, + secrets, + oidcCustomization, + ctx.logger, + ); - const remoteUrl = newRepo.clone_url; - const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; + const remoteUrl = newRepo.clone_url; + const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; - const commitResult = await initRepoPushAndProtect( - remoteUrl, - octokitOptions.auth, - ctx.workspacePath, - ctx.input.sourcePath, - defaultBranch, - protectDefaultBranch, - protectEnforceAdmins, - owner, - client, - repo, - requireCodeOwnerReviews, - bypassPullRequestAllowances, - requiredApprovingReviewCount, - restrictions, - requiredStatusCheckContexts, - requireBranchesToBeUpToDate, - requiredConversationResolution, - config, - ctx.logger, - gitCommitMessage, - gitAuthorName, - gitAuthorEmail, - dismissStaleReviews, - requiredCommitSigning, - ); + const commitResult = await initRepoPushAndProtect( + remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, + defaultBranch, + protectDefaultBranch, + protectEnforceAdmins, + owner, + client, + repo, + requireCodeOwnerReviews, + bypassPullRequestAllowances, + requiredApprovingReviewCount, + restrictions, + requiredStatusCheckContexts, + requireBranchesToBeUpToDate, + requiredConversationResolution, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + dismissStaleReviews, + requiredCommitSigning, + ); - return { - _commitHash: commitResult?.commitHash, - _remoteUrl: remoteUrl, - _repoContentsUrl: repoContentsUrl, - }; - }, - )!!; - - ctx.output('commitHash', _commitHash); - ctx.output('remoteUrl', _remoteUrl); - ctx.output('repoContentsUrl', _repoContentsUrl); + ctx.output('commitHash', commitResult?.commitHash); + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); }, }); } From 4f25522da96a08cb0e13b6acbb243ddcbb49024a Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Mon, 26 Feb 2024 22:08:17 +0100 Subject: [PATCH 400/483] wip Signed-off-by: bnechyporenko --- .changeset/kind-pants-speak.md | 2 +- .../writing-tests-for-actions.md | 2 +- packages/scaffolder-test-utils/CHANGELOG.md | 1 - .../package.json | 2 +- .../src/actions/azure.examples.test.ts | 2 +- .../src/actions/azure.test.ts | 2 +- .../package.json | 2 +- .../src/actions/bitbucketCloud.test.ts | 2 +- ...itbucketCloudPipelinesRun.examples.test.ts | 2 +- .../bitbucketCloudPipelinesRun.test.ts | 2 +- .../package.json | 2 +- .../src/actions/bitbucketServer.test.ts | 2 +- .../bitbucketServerPullRequest.test.ts | 2 +- .../package.json | 2 +- .../src/actions/bitbucket.examples.test.ts | 2 +- .../src/actions/bitbucket.test.ts | 2 +- .../package.json | 2 +- .../confluenceToMarkdown.examples.test.ts | 2 +- .../confluence/confluenceToMarkdown.test.ts | 2 +- .../package.json | 2 +- .../src/actions/fetch/cookiecutter.test.ts | 2 +- .../package.json | 2 +- .../src/actions/gerrit.test.ts | 2 +- .../src/actions/gerritReview.test.ts | 2 +- .../package.json | 2 +- .../src/actions/gitea.test.ts | 2 +- .../package.json | 2 +- .../src/actions/github.examples.test.ts | 2 +- .../src/actions/github.test.ts | 2 +- .../githubActionsDispatch.examples.test.ts | 2 +- .../src/actions/githubActionsDispatch.test.ts | 2 +- .../actions/githubAutolinks.examples.test.ts | 2 +- .../src/actions/githubAutolinks.test.ts | 2 +- .../actions/githubDeployKey.examples.test.ts | 2 +- .../src/actions/githubDeployKey.test.ts | 2 +- .../githubEnvironment.examples.test.ts | 2 +- .../src/actions/githubEnvironment.test.ts | 2 +- .../githubIssuesLabel.examples.test.ts | 2 +- .../src/actions/githubIssuesLabel.test.ts | 2 +- .../githubPullRequest.examples.test.ts | 2 +- .../src/actions/githubPullRequest.test.ts | 2 +- .../actions/githubRepoCreate.examples.test.ts | 2 +- .../src/actions/githubRepoCreate.test.ts | 2 +- .../actions/githubRepoPush.examples.test.ts | 2 +- .../src/actions/githubRepoPush.test.ts | 2 +- .../actions/githubWebhook.examples.test.ts | 2 +- .../src/actions/githubWebhook.test.ts | 2 +- .../package.json | 2 +- ...reateGitlabGroupEnsureExistsAction.test.ts | 2 +- .../actions/createGitlabIssueAction.test.ts | 2 +- ...bProjectAccessTokenAction.examples.test.ts | 2 +- ...eateGitlabProjectDeployTokenAction.test.ts | 2 +- .../src/actions/gitlab.examples.test.ts | 2 +- .../src/actions/gitlab.test.ts | 2 +- .../src/actions/gitlabMergeRequest.test.ts | 2 +- .../src/actions/gitlabRepoPush.test.ts | 2 +- .../package.json | 2 +- .../src/actions/fetch/rails/index.test.ts | 2 +- .../package.json | 2 +- .../src/actions/createProject.test.ts | 2 +- .../package.json | 2 +- .../src/actions/run/yeoman.test.ts | 2 +- plugins/scaffolder-backend/package.json | 2 +- .../builtin/catalog/fetch.examples.test.ts | 2 +- .../actions/builtin/catalog/fetch.test.ts | 2 +- .../builtin/catalog/register.examples.test.ts | 2 +- .../actions/builtin/catalog/register.test.ts | 2 +- .../builtin/catalog/write.examples.test.ts | 2 +- .../actions/builtin/catalog/write.test.ts | 2 +- .../builtin/debug/log.examples.test.ts | 2 +- .../actions/builtin/debug/log.test.ts | 2 +- .../builtin/debug/wait.examples.test.ts | 2 +- .../actions/builtin/debug/wait.test.ts | 2 +- .../builtin/fetch/plain.examples.test.ts | 2 +- .../actions/builtin/fetch/plain.test.ts | 2 +- .../builtin/fetch/plainFile.examples.test.ts | 2 +- .../actions/builtin/fetch/plainFile.test.ts | 2 +- .../builtin/fetch/template.examples.test.ts | 2 +- .../actions/builtin/fetch/template.test.ts | 2 +- .../filesystem/delete.examples.test.ts | 2 +- .../actions/builtin/filesystem/delete.test.ts | 2 +- .../filesystem/rename.examples.test.ts | 4 +- .../actions/builtin/filesystem/rename.test.ts | 2 +- .../scaffolder-node-test-utils}/.eslintrc.js | 0 .../scaffolder-node-test-utils/CHANGELOG.md | 1 + .../scaffolder-node-test-utils}/README.md | 4 +- .../scaffolder-node-test-utils}/api-report.md | 24 ++++---- .../catalog-info.yaml | 4 +- .../knip-report.md | 0 .../scaffolder-node-test-utils}/package.json | 2 +- .../src/actions/index.ts | 0 .../src/actions/mockActionConext.ts | 0 .../scaffolder-node-test-utils}/src/index.ts | 0 .../src/next/components/Stepper/Stepper.tsx | 37 +++++++----- yarn.lock | 60 +++++++++---------- 95 files changed, 152 insertions(+), 147 deletions(-) delete mode 100644 packages/scaffolder-test-utils/CHANGELOG.md rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/.eslintrc.js (100%) create mode 100644 plugins/scaffolder-node-test-utils/CHANGELOG.md rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/README.md (64%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/api-report.md (55%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/catalog-info.yaml (57%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/knip-report.md (100%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/package.json (95%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/src/actions/index.ts (100%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/src/actions/mockActionConext.ts (100%) rename {packages/scaffolder-test-utils => plugins/scaffolder-node-test-utils}/src/index.ts (100%) diff --git a/.changeset/kind-pants-speak.md b/.changeset/kind-pants-speak.md index 8af9a7e890..84b287eb00 100644 --- a/.changeset/kind-pants-speak.md +++ b/.changeset/kind-pants-speak.md @@ -12,7 +12,7 @@ '@backstage/plugin-scaffolder-backend-module-azure': patch '@backstage/plugin-scaffolder-backend-module-gitea': patch '@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/scaffolder-test-utils': patch +'@backstage/plugin-scaffolder-node-test-utils': patch '@backstage/plugin-scaffolder-backend': patch --- diff --git a/docs/features/software-templates/writing-tests-for-actions.md b/docs/features/software-templates/writing-tests-for-actions.md index 7a75250479..0c92811b6f 100644 --- a/docs/features/software-templates/writing-tests-for-actions.md +++ b/docs/features/software-templates/writing-tests-for-actions.md @@ -15,7 +15,7 @@ What is inevitably going to happen during the time. Example how to use it: ```typescript -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; const mockContext = createMockActionContext({ input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, diff --git a/packages/scaffolder-test-utils/CHANGELOG.md b/packages/scaffolder-test-utils/CHANGELOG.md deleted file mode 100644 index e290a56ced..0000000000 --- a/packages/scaffolder-test-utils/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -# @backstage/scaffolder-test-utils diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 2bbcedf897..b7902cc68a 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 7634480bc5..e00362c457 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { WebApi } from 'azure-devops-node-api'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('azure-devops-node-api', () => ({ WebApi: jest.fn(), diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts index 6b076b57d7..a23eebb93a 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts @@ -36,7 +36,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { WebApi } from 'azure-devops-node-api'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:azure', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 1ec9c4083c..b6aa2acca4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index bc56bde073..6a079a3f9b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -33,7 +33,7 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketCloud', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 1786ff26e2..472c7a6a03 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -22,7 +22,7 @@ import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index 64b4e8f7fe..9386246868 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -20,7 +20,7 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index dee14897ab..1f3abc2a8e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index 1c1ec09368..1d42e7368c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -33,7 +33,7 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketServer', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index 00d6db384a..9d2dd915f8 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -32,7 +32,7 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketServer:pull-request', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 2a88ff8525..d05b85a9de 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 17ac8ff522..c8f35fa0bf 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -36,7 +36,7 @@ import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import yaml from 'yaml'; import { sep } from 'path'; import { examples } from './bitbucket.examples'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index 0a3ddd8c9d..a63d657904 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -32,7 +32,7 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucket', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 67b899991e..d674cea8c4 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -54,7 +54,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index 47befca7ac..18ff0ad3ec 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -27,7 +27,7 @@ import { setupServer } from 'msw/node'; import { examples } from './confluenceToMarkdown.examples'; import yaml from 'yaml'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 889d59c8bf..eccb66f79e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -26,7 +26,7 @@ import { import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index a512c2ea36..6d4223a565 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0" }, diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index 80d7681e94..92a636e58b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -22,7 +22,7 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 36f7c613a7..be7abf15e1 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -49,7 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 20c8b74de6..c08544843f 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -34,7 +34,7 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:gerrit', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts index cee4b8344c..91236d4cc0 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts @@ -25,7 +25,7 @@ import { createPublishGerritReviewAction } from './gerritReview'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { commitAndPushRepo } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:gerrit:review', () => { const config = new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 2b1b4b6176..976f445ea5 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -49,7 +49,7 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index a3f7d80e88..8675b1ff97 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -19,7 +19,7 @@ import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { setupServer } from 'msw/node'; jest.mock('@backstage/plugin-scaffolder-node', () => { diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 8e1a05134f..15e148b49c 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/libsodium-wrappers": "^0.7.10", "fs-extra": "^11.2.0", "jest-when": "^3.1.0", diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts index 61b5787d46..6a6c6daf13 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.examples.test.ts @@ -37,7 +37,7 @@ import { initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index cdf2fb552a..02d6a2afaf 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -35,7 +35,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts index 9496fdd1bb..1e59037c04 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts @@ -22,7 +22,7 @@ import { import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import yaml from 'yaml'; import { examples } from './githubActionsDispatch.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts index e69a92a0a2..7f0d9b0490 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; const mockOctokit = { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts index c62cee8c00..a686ea7f6d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.examples.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createGithubAutolinksAction } from './githubAutolinks'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './githubAutolinks.examples'; import yaml from 'yaml'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts index 0a529e0377..56115c156c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createGithubAutolinksAction } from './githubAutolinks'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts index 1fb3facb83..62e79dcd68 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubDeployKeyAction } from './githubDeployKey'; import yaml from 'yaml'; import { examples } from './githubDeployKey.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts index cb8f84e563..798aa7705e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.test.ts @@ -16,7 +16,7 @@ import { createGithubDeployKeyAction } from './githubDeployKey'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts index 1130f41a68..ecb4e9f092 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createGithubEnvironmentAction } from './githubEnvironment'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index a590257a2e..16872525b4 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -15,7 +15,7 @@ */ import { createGithubEnvironmentAction } from './githubEnvironment'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 5afee9e9ed..75ac6025ce 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -15,7 +15,7 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 72200f0ea0..13c7df68cb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -20,7 +20,7 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts index 083eb78066..dd07c88a70 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts @@ -21,7 +21,7 @@ import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import yaml from 'yaml'; import { examples } from './githubPullRequest.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index 5cbaac6de6..c9d1498187 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -27,7 +27,7 @@ import { import fs from 'fs-extra'; import { createPublishGithubPullRequestAction } from './githubPullRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); 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 3dd8a44e07..843a32f758 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 @@ -29,7 +29,7 @@ import { GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGithubRepoCreateAction } from './githubRepoCreate'; import { entityRefToName } from './gitHelpers'; import yaml from 'yaml'; 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 ce29de7a1e..25ddd176c1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -15,7 +15,7 @@ */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('./gitHelpers', () => { return { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts index 8c48811eb1..691cebd367 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.examples.test.ts @@ -29,7 +29,7 @@ import { TemplateAction, initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts index 31b3a7c95e..206d4f067d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts @@ -60,7 +60,7 @@ import { initRepoAndPush, } from '@backstage/plugin-scaffolder-node'; import { ConfigReader } from '@backstage/config'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts index 17dd371a21..d58d737f93 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { DefaultGithubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts index 8c5a57542f..cbdcee6af1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.test.ts @@ -20,7 +20,7 @@ import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 72514db885..f24a967637 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -57,7 +57,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "jest-date-mock": "^1.0.8" }, "files": [ diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts index a94a844d4c..4dc0305c2e 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts @@ -15,7 +15,7 @@ */ import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index d33cbb37dc..820ef8902e 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createGitlabIssueAction, IssueType } from './createGitlabIssueAction'; import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts index 90f4fca283..5afeaba8ae 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.examples.test.ts @@ -18,7 +18,7 @@ import { ScmIntegrations } from '@backstage/integration'; import yaml from 'yaml'; import { createGitlabProjectAccessTokenAction } from './createGitlabProjectAccessTokenAction'; // Adjust the import based on your project structure import { examples } from './createGitlabProjectAccessTokenAction.examples'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { DateTime } from 'luxon'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts index c626a20124..cab72c9a6d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts @@ -15,7 +15,7 @@ */ import { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts index 3bd6b0e5ff..4294e5f137 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import yaml from 'yaml'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => { return { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts index 40712966ba..bbcae08063 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts @@ -30,7 +30,7 @@ import { createPublishGitlabAction } from './gitlab'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; const mockGitlabClient = { Namespaces: { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index 4f6585e7b3..befc2c01c9 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -19,7 +19,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts index 24ba3abcd3..569d3c0c56 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.test.ts @@ -19,7 +19,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createGitlabRepoPushAction } from './gitlabRepoPush'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 5426049996..59950702c6 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -51,7 +51,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0", "@types/node": "^18.17.8", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 0f56bd55ec..b43ffcc95c 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -34,7 +34,7 @@ import { resolve as resolvePath } from 'path'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index fb942617f3..3b1cfcc276 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/types": "workspace:^", "msw": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 715f21d944..158b7db4ae 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -15,7 +15,7 @@ */ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 9abfe95ac0..6631d7833c 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -39,7 +39,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/types": "workspace:^", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" diff --git a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts index ae18a25365..52bd95a3f9 100644 --- a/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts +++ b/plugins/scaffolder-backend-module-yeoman/src/actions/run/yeoman.test.ts @@ -18,7 +18,7 @@ import { yeomanRun } from './yeomanRun'; jest.mock('./yeomanRun'); -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import os from 'os'; import { createRunYeomanAction } from './yeoman'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 5884450a89..61bd5ef49b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -95,7 +95,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/scaffolder-test-utils": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@types/fs-extra": "^11.0.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts index 62e0cef0dc..a368144784 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index 43d7660a9a..d3fd398a90 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts index e9900221e3..db3d7151d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts index b035a8c94d..8f6219b64c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts index 6f410db07e..e6b4bcde47 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts @@ -20,7 +20,7 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; import * as yaml from 'yaml'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts index 1b06c930f9..cc22b75da2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts @@ -21,7 +21,7 @@ jest.mock('fs-extra'); const fsMock = fs as jest.Mocked; import os from 'os'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts index de008433f0..06addba98d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index c9bd0f8cbe..09c090582a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 00574dedfd..8bc29a74c9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -18,7 +18,7 @@ import { createWaitAction } from './wait'; import { Writable } from 'stream'; import { examples } from './wait.examples'; import yaml from 'yaml'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('debug:wait examples', () => { const action = createWaitAction(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 1424ca3012..0dcbd10f0f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -16,7 +16,7 @@ import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts index 7ce945a08f..82a32fcbac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts @@ -22,7 +22,7 @@ import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainAction } from './plain'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './plain.examples'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index 7468779f3a..bc20fafe19 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -20,7 +20,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { }); import { resolve as resolvePath } from 'path'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts index 25993caf96..9978d6d8f5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => { const actual = jest.requireActual('@backstage/plugin-scaffolder-node'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts index 8f889bef4b..bf40c7ad0e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts @@ -20,7 +20,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { }); import { resolve as resolvePath } from 'path'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index 29a267ceab..36f1f00b50 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -18,7 +18,7 @@ import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; import { resolvePackagePath, UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { createFetchTemplateAction } from './template'; import { ActionContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 3cd8f61e77..fd0ef7d757 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -30,7 +30,7 @@ import { TemplateAction, } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; type FetchTemplateInput = ReturnType< typeof createFetchTemplateAction diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts index 8611486dac..d11b1e4087 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -15,7 +15,7 @@ */ import { createFilesystemDeleteAction } from './delete'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { resolve as resolvePath } from 'path'; import fs from 'fs-extra'; import yaml from 'yaml'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index 2cddb9f5a6..8226133376 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -16,7 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemDeleteAction } from './delete'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts index 5e9ba84465..0881bb8260 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -16,7 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './rename.examples'; @@ -32,7 +32,7 @@ describe('fs:rename examples', () => { const mockContext = createMockActionContext({ input: { - files: files, + files, }, workspacePath, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index b081200b71..6696fda693 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -16,7 +16,7 @@ import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; -import { createMockActionContext } from '@backstage/scaffolder-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import fs from 'fs-extra'; import { createMockDirectory } from '@backstage/backend-test-utils'; diff --git a/packages/scaffolder-test-utils/.eslintrc.js b/plugins/scaffolder-node-test-utils/.eslintrc.js similarity index 100% rename from packages/scaffolder-test-utils/.eslintrc.js rename to plugins/scaffolder-node-test-utils/.eslintrc.js diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md new file mode 100644 index 0000000000..2943a2a755 --- /dev/null +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -0,0 +1 @@ +# @backstage/plugin-scaffolder-node-test-utils diff --git a/packages/scaffolder-test-utils/README.md b/plugins/scaffolder-node-test-utils/README.md similarity index 64% rename from packages/scaffolder-test-utils/README.md rename to plugins/scaffolder-node-test-utils/README.md index e5810058b3..851ddf808d 100644 --- a/packages/scaffolder-test-utils/README.md +++ b/plugins/scaffolder-node-test-utils/README.md @@ -1,4 +1,4 @@ -# @backstage/scaffolder-test-utils +# @backstage/plugin-scaffolder-node-test-utils Contains utilities that can be used when testing scaffolder features. @@ -8,5 +8,5 @@ Install the package via Yarn into your own packages: ```sh cd # if within a monorepo -yarn add --dev @backstage/scaffolder-test-utils +yarn add --dev @backstage/plugin-scaffolder-node-test-utils ``` diff --git a/packages/scaffolder-test-utils/api-report.md b/plugins/scaffolder-node-test-utils/api-report.md similarity index 55% rename from packages/scaffolder-test-utils/api-report.md rename to plugins/scaffolder-node-test-utils/api-report.md index 4e50a55f5e..fcaaadfd67 100644 --- a/packages/scaffolder-test-utils/api-report.md +++ b/plugins/scaffolder-node-test-utils/api-report.md @@ -1,32 +1,32 @@ -## API Report File for "@backstage/scaffolder-test-utils" +## API Report File for "@backstage/plugin-scaffolder-node-test-utils" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts /// -import { ActionContext } from '@backstage/plugin-scaffolder-node'; -import { JsonObject } from '@backstage/types'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; -import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import {ActionContext} from './index'; +import {JsonObject} from '@backstage/types'; +import {TaskSecrets} from './index'; +import {TemplateInfo} from './index'; import * as winston from 'winston'; -import { Writable } from 'stream'; +import {Writable} from 'stream'; // @public export const createMockActionContext: < - TActionInput extends JsonObject = JsonObject, - TActionOutput extends JsonObject = JsonObject, + TActionInput extends JsonObject = JsonObject, + TActionOutput extends JsonObject = JsonObject, >( - options?: - | { + options?: + | { input?: TActionInput | undefined; logger?: winston.Logger | undefined; logStream?: Writable | undefined; secrets?: TaskSecrets | undefined; templateInfo?: TemplateInfo | undefined; workspacePath?: string | undefined; - } - | undefined, + } + | undefined, ) => ActionContext; // (No @packageDocumentation comment for this package) diff --git a/packages/scaffolder-test-utils/catalog-info.yaml b/plugins/scaffolder-node-test-utils/catalog-info.yaml similarity index 57% rename from packages/scaffolder-test-utils/catalog-info.yaml rename to plugins/scaffolder-node-test-utils/catalog-info.yaml index 596e9b1f64..980bd07e34 100644 --- a/packages/scaffolder-test-utils/catalog-info.yaml +++ b/plugins/scaffolder-node-test-utils/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-scaffolder-test-utils - title: '@backstage/scaffolder-test-utils' + name: backstage-plugin-scaffolder-node-test-utils + title: '@backstage/plugin-scaffolder-node-test-utils' spec: lifecycle: experimental type: backstage-node-library diff --git a/packages/scaffolder-test-utils/knip-report.md b/plugins/scaffolder-node-test-utils/knip-report.md similarity index 100% rename from packages/scaffolder-test-utils/knip-report.md rename to plugins/scaffolder-node-test-utils/knip-report.md diff --git a/packages/scaffolder-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json similarity index 95% rename from packages/scaffolder-test-utils/package.json rename to plugins/scaffolder-node-test-utils/package.json index b1aaad001e..21bbb83a91 100644 --- a/packages/scaffolder-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/scaffolder-test-utils", + "name": "@backstage/plugin-scaffolder-node-test-utils", "version": "0.0.1", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/scaffolder-test-utils/src/actions/index.ts b/plugins/scaffolder-node-test-utils/src/actions/index.ts similarity index 100% rename from packages/scaffolder-test-utils/src/actions/index.ts rename to plugins/scaffolder-node-test-utils/src/actions/index.ts diff --git a/packages/scaffolder-test-utils/src/actions/mockActionConext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts similarity index 100% rename from packages/scaffolder-test-utils/src/actions/mockActionConext.ts rename to plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts diff --git a/packages/scaffolder-test-utils/src/index.ts b/plugins/scaffolder-node-test-utils/src/index.ts similarity index 100% rename from packages/scaffolder-test-utils/src/index.ts rename to plugins/scaffolder-node-test-utils/src/index.ts diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 9b475870d2..9fdc0270e7 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -37,9 +37,8 @@ import { type FormValidation, } from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; -import { useTemplateSchema } from '../../hooks/useTemplateSchema'; +import { useTemplateSchema, useFormDataFromQuery } from '../../hooks'; import validator from '@rjsf/validator-ajv8'; -import { useFormDataFromQuery } from '../../hooks'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; import { hasErrors } from './utils'; import * as FieldOverrides from './FieldOverrides'; @@ -112,6 +111,18 @@ export const Stepper = (stepperProps: StepperProps) => { const [errors, setErrors] = useState(); const styles = useStyles(); + const templateName = + typeof formState.name === 'string' + ? formState.name + : props.templateName ?? 'unknown'; + + const backLabel = + presentation?.buttonLabels?.backButtonText ?? backButtonText; + const createLabel = + presentation?.buttonLabels?.createButtonText ?? createButtonText; + const reviewLabel = + presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText; + const extensions = useMemo(() => { return Object.fromEntries( props.extensions.map(({ name, component }) => [name, component]), @@ -147,10 +158,8 @@ export const Stepper = (stepperProps: StepperProps) => { const handleCreate = useCallback(() => { props.onCreate(formState); - const name = - typeof formState.name === 'string' ? formState.name : undefined; - analytics.captureEvent('create', name ?? props.templateName ?? 'unknown'); - }, [props, formState, analytics]); + analytics.captureEvent('click', `[${templateName}]: ${createLabel}`); + }, [props, formState, analytics, templateName, createLabel]); const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); @@ -174,20 +183,16 @@ export const Stepper = (stepperProps: StepperProps) => { setErrors(undefined); setActiveStep(prevActiveStep => { const stepNum = prevActiveStep + 1; - analytics.captureEvent('click', `Next Step (${stepNum})`); + analytics.captureEvent( + 'click', + `[${templateName}]: Next Step (${stepNum})`, + ); return stepNum; }); } setFormState(current => ({ ...current, ...formData })); }; - const backLabel = - presentation?.buttonLabels?.backButtonText ?? backButtonText; - const createLabel = - presentation?.buttonLabels?.createButtonText ?? createButtonText; - const reviewLabel = - presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText; - return ( <> {isValidating && } @@ -214,7 +219,7 @@ export const Stepper = (stepperProps: StepperProps) => { ); })} - Review + ${reviewLabel}
@@ -274,7 +279,7 @@ export const Stepper = (stepperProps: StepperProps) => { className={styles.backButton} disabled={activeStep < 1} > - Back + {backLabel} - - + diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 8328da1c6d..6bcb6badc7 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -13,296 +13,162 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react'; -import { - Box, - Button, - IconButton, - makeStyles, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from '@material-ui/core'; -import { - Notification, - NotificationType, -} from '@backstage/plugin-notifications-common'; -import { useNavigate } from 'react-router-dom'; -import Checkbox from '@material-ui/core/Checkbox'; -import Check from '@material-ui/icons/Check'; -import Bookmark from '@material-ui/icons/Bookmark'; +import React, { useMemo } from 'react'; +import throttle from 'lodash/throttle'; +import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Notification } from '@backstage/plugin-notifications-common'; import { notificationsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; -import Inbox from '@material-ui/icons/Inbox'; -import CloseIcon from '@material-ui/icons/Close'; +import MarkAsUnreadIcon from '@material-ui/icons/Markunread'; +import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; + // @ts-ignore import RelativeTime from 'react-relative-time'; -import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; +import { Link, Table, TableColumn } from '@backstage/core-components'; -const useStyles = makeStyles(theme => ({ - table: { - border: `1px solid ${theme.palette.divider}`, - }, - header: { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - - notificationRow: { - cursor: 'pointer', - '&.unread': { - border: '1px solid rgba(255, 255, 255, .3)', - }, - '& .hideOnHover': { - display: 'initial', - }, - '& .showOnHover': { - display: 'none', - }, - '&:hover': { - '& .hideOnHover': { - display: 'none', - }, - '& .showOnHover': { - display: 'initial', - }, - }, - }, - actionButton: { - padding: '9px', - }, - checkBox: { - padding: '0 10px 10px 0', - }, -})); +const ThrottleDelayMs = 1000; /** @public */ -export const NotificationsTable = (props: { - onUpdate: () => void; - type: NotificationType; +export type NotificationsTableProps = { + isLoading?: boolean; notifications?: Notification[]; -}) => { - const { notifications, type } = props; - const navigate = useNavigate(); - const styles = useStyles(); - const [selected, setSelected] = useState([]); + onUpdate: () => void; + setContainsText: (search: string) => void; +}; + +/** @public */ +export const NotificationsTable = ({ + isLoading, + notifications = [], + onUpdate, + setContainsText, +}: NotificationsTableProps) => { const notificationsApi = useApi(notificationsApiRef); - const onCheckBoxClick = (id: string) => { - const index = selected.indexOf(id); - if (index !== -1) { - setSelected(selected.filter(s => s !== id)); - } else { - setSelected([...selected, id]); - } - }; + const onSwitchReadStatus = React.useCallback( + (notification: Notification) => { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: !notification.read, + }) + .then(() => onUpdate()); + }, + [notificationsApi, onUpdate], + ); - useEffect(() => { - setSelected([]); - }, [type]); + const throttledContainsTextHandler = useMemo( + () => throttle(setContainsText, ThrottleDelayMs), + [setContainsText], + ); - const isChecked = (id: string) => { - return selected.indexOf(id) !== -1; - }; - - const isAllSelected = () => { - return ( - selected.length === notifications?.length && notifications.length > 0 - ); - }; - - return ( -

- - - - {type !== 'saved' && !notifications?.length && 'No notifications'} - {type !== 'saved' && !!notifications?.length && ( - { - if (isAllSelected()) { - setSelected([]); - } else { - setSelected( - notifications ? notifications.map(n => n.id) : [], - ); - } - }} - /> - )} - {type === 'saved' && - `${notifications?.length ?? 0} saved notifications`} - {selected.length === 0 && - !!notifications?.length && - type !== 'saved' && - 'Select all'} - {selected.length > 0 && `${selected.length} selected`} - {type === 'done' && selected.length > 0 && ( - - )} - - {type === 'undone' && selected.length > 0 && ( - - )} - - - - - {props.notifications?.map(notification => { + const compactColumns = React.useMemo( + (): TableColumn[] => [ + { + customFilterAndSearch: () => + true /* Keep it on backend due to pagination. If recent flickering is an issue, implement search here as well. */, + render: (notification: Notification) => { + // Compact content return ( - - - onCheckBoxClick(notification.id)} - /> - - - notificationsApi - .updateNotifications({ ids: [notification.id], read: true }) - .then(() => navigate(notification.payload.link)) - } - style={{ paddingLeft: 0 }} - > + <> + - {notification.payload.title} + {notification.payload.link ? ( + + {notification.payload.title} + + ) : ( + notification.payload.title + )} {notification.payload.description} - - - - - - - - - notificationsApi - .updateNotifications({ - ids: [notification.id], - read: true, - }) - .then(() => navigate(notification.payload.link)) - } - > - - - - - { - if (notification.read) { - notificationsApi - .updateNotifications({ - ids: [notification.id], - done: false, - }) - .then(() => { - props.onUpdate(); - }); - } else { - notificationsApi - .updateNotifications({ - ids: [notification.id], - done: true, - }) - .then(() => { - props.onUpdate(); - }); - } - }} - > - {notification.read ? ( - - ) : ( - - )} - - - - { - if (notification.saved) { - notificationsApi - .updateNotifications({ - ids: [notification.id], - saved: false, - }) - .then(() => { - props.onUpdate(); - }); - } else { - notificationsApi - .updateNotifications({ - ids: [notification.id], - saved: true, - }) - .then(() => { - props.onUpdate(); - }); - } - }} - > - {notification.saved ? ( - - ) : ( - - )} - - - - - + + {notification.origin && ( + <>{notification.origin} •  + )} + {notification.payload.topic && ( + <>{notification.payload.topic} •  + )} + {notification.created && ( + + )} + + + ); - })} - -
+ }, + }, + // { + // // TODO: additional action links + // width: '25%', + // render: (notification: Notification) => { + // return ( + // notification.payload.link && ( + // + // {/* TODO: render additionalLinks of different titles */} + // + // + //  More info + // + // + // + // ) + // ); + // }, + // }, + { + // TODO: action for saving notifications + // actions + width: '1rem', + render: (notification: Notification) => { + const markAsReadText = !!notification.read + ? 'Return among unread' + : 'Mark as read'; + const IconComponent = !!notification.read + ? MarkAsUnreadIcon + : MarkAsReadIcon; + + return ( + + { + onSwitchReadStatus(notification); + }} + > + + + + ); + }, + }, + ], + [onSwitchReadStatus], + ); + + // TODO: render "Saved notifications" as "Pinned" + return ( + + isLoading={isLoading} + options={{ + search: true, + // TODO: add pagination + // paging: true, + // pageSize, + header: false, + sorting: false, + }} + // onPageChange={setPageNumber} + // onRowsPerPageChange={setPageSize} + // page={offset} + // totalCount={value?.totalCount} + onSearchChange={throttledContainsTextHandler} + data={notifications} + columns={compactColumns} + /> ); }; diff --git a/yarn.lock b/yarn.lock index 5bfd5cac1a..2e603fba32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7714,6 +7714,7 @@ __metadata: "@testing-library/react": ^14.0.0 "@testing-library/user-event": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 + lodash: ^4.17.21 msw: ^1.0.0 react-relative-time: ^0.0.9 react-use: ^17.2.4 From 9823e313c31d233c17c397f268cf3d84f506bf1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 23 Feb 2024 17:19:50 +0100 Subject: [PATCH 405/483] backend-plugin-api: add support for limited user tokens Signed-off-by: Patrik Oldsberg --- .../auth/authServiceFactory.ts | 22 ++++++++ .../src/auth/createLegacyAuthAdapters.ts | 21 +++++++ packages/backend-plugin-api/api-report.md | 14 ++++- .../src/services/definitions/AuthService.ts | 11 +++- packages/backend-test-utils/api-report.md | 11 ++++ .../src/next/services/MockAuthService.test.ts | 44 +++++++++++++++ .../src/next/services/MockAuthService.ts | 46 ++++++++++++++- .../src/next/services/mockCredentials.test.ts | 32 +++++++++++ .../src/next/services/mockCredentials.ts | 56 +++++++++++++++++++ .../src/next/services/mockServices.ts | 1 + 10 files changed, 254 insertions(+), 4 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 19dfbe1299..4473acd94b 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -111,6 +111,7 @@ class DefaultAuthService implements AuthService { private readonly disableDefaultAuthPolicy: boolean, ) {} + // allowLimitedAccess is currently ignored, since we currently always use the full user tokens async authenticate(token: string): Promise { const { sub, aud } = decodeJwt(token); @@ -193,6 +194,27 @@ class DefaultAuthService implements AuthService { ); } } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + const internalCredentials = toInternalBackstageCredentials(credentials); + + const { token } = internalCredentials; + + if (!token) { + throw new AuthenticationError( + 'User credentials is unexpectedly missing token', + ); + } + + const { exp } = decodeJwt(token); + if (!exp) { + throw new AuthenticationError('User token is missing expiration'); + } + + return { token, expiresAt: new Date(exp * 1000) }; + } } /** @public */ diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index d12dd8b9fc..29fad9b579 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -123,6 +123,27 @@ class AuthCompat implements AuthService { ); } } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + const internalCredentials = toInternalBackstageCredentials(credentials); + + const { token } = internalCredentials; + + if (!token) { + throw new AuthenticationError( + 'User credentials is unexpectedly missing token', + ); + } + + const { exp } = decodeJwt(token); + if (!exp) { + throw new AuthenticationError('User token is missing expiration'); + } + + return { token, expiresAt: new Date(exp * 1000) }; + } } function getTokenFromRequest(req: Request) { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index cb44ba158d..ec63ea9062 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -24,7 +24,19 @@ import { Response as Response_2 } from 'express'; // @public (undocumented) export interface AuthService { // (undocumented) - authenticate(token: string): Promise; + authenticate( + token: string, + options?: { + allowLimitedAccess?: boolean; + }, + ): Promise; + // (undocumented) + getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ + token: string; + expiresAt: Date; + }>; // (undocumented) getOwnServiceCredentials(): Promise< BackstageCredentials diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index eab0a7fac7..391087cf2f 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -63,7 +63,12 @@ export type BackstagePrincipalTypes = { * @public */ export interface AuthService { - authenticate(token: string): Promise; + authenticate( + token: string, + options?: { + allowLimitedAccess?: boolean; + }, + ): Promise; isPrincipal( credentials: BackstageCredentials, @@ -78,4 +83,8 @@ export interface AuthService { onBehalfOf: BackstageCredentials; targetPluginId: string; }): Promise<{ token: string }>; + + getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }>; } diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d0e37a476d..4991164013 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -50,6 +50,17 @@ export function isDockerDisabledForTests(): boolean; // @public (undocumented) export namespace mockCredentials { + export function limitedUser( + userEntityRef?: string, + ): BackstageCredentials; + export namespace limitedUser { + export function header(userEntityRef?: string): string; + // (undocumented) + export function invalidHeader(): string; + // (undocumented) + export function invalidToken(): string; + export function token(userEntityRef?: string): string; + } export function none(): BackstageCredentials; export namespace none { export function header(): string; diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts index dfb54b9762..81e56d592c 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts @@ -53,6 +53,12 @@ describe('MockAuthService', () => { auth.authenticate(mockCredentials.user.token()), ).resolves.toEqual(mockCredentials.user()); + await expect( + auth.authenticate(mockCredentials.user.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + await expect( auth.authenticate(mockCredentials.user.token()), ).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF)); @@ -66,6 +72,44 @@ describe('MockAuthService', () => { ).rejects.toThrow('User token is invalid'); }); + it('should authenticate mock limited user tokens', async () => { + await expect( + auth.authenticate(mockCredentials.limitedUser.token()), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), {}), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: false, + }), + ).rejects.toThrow('Limited user token is not allowed'); + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + auth.authenticate(mockCredentials.limitedUser.token(), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user(DEFAULT_MOCK_USER_ENTITY_REF)); + + await expect( + auth.authenticate( + mockCredentials.limitedUser.token('user:default/other'), + { + allowLimitedAccess: true, + }, + ), + ).resolves.toEqual(mockCredentials.user('user:default/other')); + + await expect( + auth.authenticate(mockCredentials.limitedUser.invalidToken()), + ).rejects.toThrow('Limited user token is invalid'); + }); + it('should authenticate mock service tokens', async () => { await expect( auth.authenticate(mockCredentials.service.token()), diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 6a18711798..0d8945cd9d 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -27,9 +27,12 @@ import { mockCredentials, MOCK_USER_TOKEN, MOCK_USER_TOKEN_PREFIX, + MOCK_INVALID_USER_TOKEN, + MOCK_USER_LIMITED_TOKEN, + MOCK_USER_LIMITED_TOKEN_PREFIX, + MOCK_INVALID_USER_LIMITED_TOKEN, MOCK_SERVICE_TOKEN, MOCK_SERVICE_TOKEN_PREFIX, - MOCK_INVALID_USER_TOKEN, MOCK_INVALID_SERVICE_TOKEN, UserTokenPayload, ServiceTokenPayload, @@ -48,14 +51,24 @@ export class MockAuthService implements AuthService { this.disableDefaultAuthPolicy = options.disableDefaultAuthPolicy; } - async authenticate(token: string): Promise { + async authenticate( + token: string, + options?: { allowLimitedAccess?: boolean }, + ): Promise { switch (token) { case MOCK_USER_TOKEN: return mockCredentials.user(); + case MOCK_USER_LIMITED_TOKEN: + if (!options?.allowLimitedAccess) { + throw new AuthenticationError('Limited user token is not allowed'); + } + return mockCredentials.user(); case MOCK_SERVICE_TOKEN: return mockCredentials.service(); case MOCK_INVALID_USER_TOKEN: throw new AuthenticationError('User token is invalid'); + case MOCK_INVALID_USER_LIMITED_TOKEN: + throw new AuthenticationError('Limited user token is invalid'); case MOCK_INVALID_SERVICE_TOKEN: throw new AuthenticationError('Service token is invalid'); case '': @@ -72,6 +85,18 @@ export class MockAuthService implements AuthService { return mockCredentials.user(userEntityRef); } + if (token.startsWith(MOCK_USER_LIMITED_TOKEN_PREFIX)) { + if (!options?.allowLimitedAccess) { + throw new AuthenticationError('Limited user token is not allowed'); + } + + const { sub: userEntityRef }: UserTokenPayload = JSON.parse( + token.slice(MOCK_USER_LIMITED_TOKEN_PREFIX.length), + ); + + return mockCredentials.user(userEntityRef); + } + if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) { const { sub, target, obo }: ServiceTokenPayload = JSON.parse( token.slice(MOCK_SERVICE_TOKEN_PREFIX.length), @@ -144,4 +169,21 @@ export class MockAuthService implements AuthService { }), }; } + + async getLimitedUserToken( + credentials: BackstageCredentials, + ): Promise<{ token: string; expiresAt: Date }> { + if (credentials.principal.type !== 'user') { + throw new AuthenticationError( + `Refused to issue limited user token for credential type '${credentials.principal.type}'`, + ); + } + + return { + token: mockCredentials.limitedUser.token( + credentials.principal.userEntityRef, + ), + expiresAt: new Date(Date.now() + 3600), + }; + } } diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index 67cda92be8..3d72f362cd 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -36,6 +36,18 @@ describe('mockCredentials', () => { }); }); + it('creates a mocked credentials object for a limited user principal', () => { + expect(mockCredentials.limitedUser()).toEqual({ + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/mock' }, + }); + + expect(mockCredentials.limitedUser('user:default/other')).toEqual({ + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/other' }, + }); + }); + it('creates a mocked credentials object for a service principal', () => { expect(mockCredentials.service()).toEqual({ $$type: '@backstage/BackstageCredentials', @@ -68,6 +80,26 @@ describe('mockCredentials', () => { ); }); + it('creates limited user tokens and headers', () => { + expect(mockCredentials.limitedUser.token()).toBe('mock-limited-user-token'); + expect(mockCredentials.limitedUser.token('user:default/other')).toBe( + 'mock-limited-user-token:{"sub":"user:default/other"}', + ); + expect(mockCredentials.limitedUser.invalidToken()).toBe( + 'mock-invalid-limited-user-token', + ); + + expect(mockCredentials.limitedUser.header()).toBe( + 'Bearer mock-limited-user-token', + ); + expect(mockCredentials.limitedUser.header('user:default/other')).toBe( + 'Bearer mock-limited-user-token:{"sub":"user:default/other"}', + ); + expect(mockCredentials.limitedUser.invalidHeader()).toBe( + 'Bearer mock-invalid-limited-user-token', + ); + }); + it('creates service tokens and headers', () => { expect(mockCredentials.service.token()).toBe('mock-service-token'); expect( diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index 8dac9c1a4b..72ffbc7af2 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -25,9 +25,16 @@ export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock'; export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service'; export const MOCK_NONE_TOKEN = 'mock-none-token'; + export const MOCK_USER_TOKEN = 'mock-user-token'; export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:'; export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token'; + +export const MOCK_USER_LIMITED_TOKEN = 'mock-limited-user-token'; +export const MOCK_USER_LIMITED_TOKEN_PREFIX = 'mock-limited-user-token:'; +export const MOCK_INVALID_USER_LIMITED_TOKEN = + 'mock-invalid-limited-user-token'; + export const MOCK_SERVICE_TOKEN = 'mock-service-token'; export const MOCK_SERVICE_TOKEN_PREFIX = 'mock-service-token:'; export const MOCK_INVALID_SERVICE_TOKEN = 'mock-invalid-service-token'; @@ -143,6 +150,55 @@ export namespace mockCredentials { } } + /** + * Creates a mocked credentials object for a user principal with limited + * access. + * + * The default user entity reference is 'user:default/mock'. + */ + export function limitedUser( + userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF, + ): BackstageCredentials { + return user(userEntityRef); + } + + /** + * Utilities related to limited user credentials. + */ + export namespace limitedUser { + /** + * Creates a mocked limited user token. If a payload is provided it will be + * encoded into the token and forwarded to the credentials object when + * authenticated by the mock auth service. + */ + export function token(userEntityRef?: string): string { + if (userEntityRef) { + validateUserEntityRef(userEntityRef); + return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({ + sub: userEntityRef, + } satisfies UserTokenPayload)}`; + } + return MOCK_USER_LIMITED_TOKEN; + } + + /** + * Returns an authorization header with a mocked limited user token. If a + * payload is provided it will be encoded into the token and forwarded to + * the credentials object when authenticated by the mock auth service. + */ + export function header(userEntityRef?: string): string { + return `Bearer ${token(userEntityRef)}`; + } + + export function invalidToken(): string { + return MOCK_INVALID_USER_LIMITED_TOKEN; + } + + export function invalidHeader(): string { + return `Bearer ${invalidToken()}`; + } + } + /** * Creates a mocked credentials object for a service principal. * diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 7300b34105..f6d2d39167 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -205,6 +205,7 @@ export namespace mockServices { getOwnServiceCredentials: jest.fn(), isPrincipal: jest.fn() as any, getPluginRequestToken: jest.fn(), + getLimitedUserToken: jest.fn(), })); } From 982fc43d68d7122c65d816f9d2486b2b46238502 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 25 Feb 2024 12:09:51 +0100 Subject: [PATCH 406/483] backend-plugin-api: add AuthService.getNoneCredentials Signed-off-by: Patrik Oldsberg --- .../services/implementations/auth/authServiceFactory.ts | 6 ++++++ .../backend-common/src/auth/createLegacyAuthAdapters.ts | 7 +++++++ .../src/services/definitions/AuthService.ts | 2 ++ .../src/next/services/MockAuthService.test.ts | 6 ++++++ .../src/next/services/MockAuthService.ts | 4 ++++ 5 files changed, 25 insertions(+) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 4473acd94b..93a889654a 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -157,6 +157,12 @@ class DefaultAuthService implements AuthService { return true; } + async getNoneCredentials(): Promise< + BackstageCredentials + > { + return createCredentialsWithNonePrincipal(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 29fad9b579..fd44ca5b91 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -17,6 +17,7 @@ import { AuthService, BackstageCredentials, + BackstageNonePrincipal, BackstagePrincipalTypes, BackstageServicePrincipal, BackstageUserInfo, @@ -65,6 +66,12 @@ class AuthCompat implements AuthService { return true; } + async getNoneCredentials(): Promise< + BackstageCredentials + > { + return createCredentialsWithNonePrincipal(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index 391087cf2f..f9ff7edadc 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -75,6 +75,8 @@ export interface AuthService { type: TType, ): credentials is BackstageCredentials; + getNoneCredentials(): Promise>; + getOwnServiceCredentials(): Promise< BackstageCredentials >; diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts index 81e56d592c..4343027aea 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts @@ -157,6 +157,12 @@ describe('MockAuthService', () => { ).rejects.toThrow('Service token is invalid'); }); + it('should return none credentials', async () => { + await expect(auth.getNoneCredentials()).resolves.toEqual( + mockCredentials.none(), + ); + }); + it('should return own service credentials', async () => { await expect(auth.getOwnServiceCredentials()).resolves.toEqual( mockCredentials.service('plugin:test'), diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 0d8945cd9d..4977c4df0e 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -117,6 +117,10 @@ export class MockAuthService implements AuthService { throw new AuthenticationError(`Unknown mock token '${token}'`); } + async getNoneCredentials() { + return mockCredentials.none(); + } + async getOwnServiceCredentials(): Promise< BackstageCredentials > { From e2108005452c44bc464499ee57dca65d78874b2e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 01:25:27 +0100 Subject: [PATCH 407/483] backend-test-utils: update mockCredentials for cookie auth Signed-off-by: Patrik Oldsberg --- .../src/next/services/mockCredentials.test.ts | 12 +++------ .../src/next/services/mockCredentials.ts | 26 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index 3d72f362cd..ed0071d4b8 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -81,7 +81,6 @@ describe('mockCredentials', () => { }); it('creates limited user tokens and headers', () => { - expect(mockCredentials.limitedUser.token()).toBe('mock-limited-user-token'); expect(mockCredentials.limitedUser.token('user:default/other')).toBe( 'mock-limited-user-token:{"sub":"user:default/other"}', ); @@ -89,14 +88,11 @@ describe('mockCredentials', () => { 'mock-invalid-limited-user-token', ); - expect(mockCredentials.limitedUser.header()).toBe( - 'Bearer mock-limited-user-token', + expect(mockCredentials.limitedUser.cookie('user:default/other')).toBe( + 'backstage-auth=mock-limited-user-token:{"sub":"user:default/other"}', ); - expect(mockCredentials.limitedUser.header('user:default/other')).toBe( - 'Bearer mock-limited-user-token:{"sub":"user:default/other"}', - ); - expect(mockCredentials.limitedUser.invalidHeader()).toBe( - 'Bearer mock-invalid-limited-user-token', + expect(mockCredentials.limitedUser.invalidCookie()).toBe( + 'backstage-auth=mock-invalid-limited-user-token', ); }); diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index 72ffbc7af2..16d2381c73 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -24,13 +24,14 @@ import { export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock'; export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service'; +export const MOCK_AUTH_COOKIE = 'backstage-auth'; + export const MOCK_NONE_TOKEN = 'mock-none-token'; export const MOCK_USER_TOKEN = 'mock-user-token'; export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:'; export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token'; -export const MOCK_USER_LIMITED_TOKEN = 'mock-limited-user-token'; export const MOCK_USER_LIMITED_TOKEN_PREFIX = 'mock-limited-user-token:'; export const MOCK_INVALID_USER_LIMITED_TOKEN = 'mock-invalid-limited-user-token'; @@ -171,14 +172,13 @@ export namespace mockCredentials { * encoded into the token and forwarded to the credentials object when * authenticated by the mock auth service. */ - export function token(userEntityRef?: string): string { - if (userEntityRef) { - validateUserEntityRef(userEntityRef); - return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({ - sub: userEntityRef, - } satisfies UserTokenPayload)}`; - } - return MOCK_USER_LIMITED_TOKEN; + export function token( + userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF, + ): string { + validateUserEntityRef(userEntityRef); + return `${MOCK_USER_LIMITED_TOKEN_PREFIX}${JSON.stringify({ + sub: userEntityRef, + } satisfies UserTokenPayload)}`; } /** @@ -186,16 +186,16 @@ export namespace mockCredentials { * payload is provided it will be encoded into the token and forwarded to * the credentials object when authenticated by the mock auth service. */ - export function header(userEntityRef?: string): string { - return `Bearer ${token(userEntityRef)}`; + export function cookie(userEntityRef?: string): string { + return `${MOCK_AUTH_COOKIE}=${token(userEntityRef)}`; } export function invalidToken(): string { return MOCK_INVALID_USER_LIMITED_TOKEN; } - export function invalidHeader(): string { - return `Bearer ${invalidToken()}`; + export function invalidCookie(): string { + return `${MOCK_AUTH_COOKIE}=${invalidToken()}`; } } From d455112cbf59f680202c7dc8d4354da1ff85e379 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Feb 2024 01:28:27 +0100 Subject: [PATCH 408/483] backend-plugin-api: updated cookie auth implementation Signed-off-by: Patrik Oldsberg --- .../auth/authServiceFactory.ts | 15 +- .../httpAuth/httpAuthServiceFactory.ts | 197 ++++++++++++------ .../createCredentialsBarrier.test.ts | 59 +++++- .../httpRouter/createCredentialsBarrier.ts | 2 +- .../src/auth/createLegacyAuthAdapters.ts | 67 +++--- packages/backend-plugin-api/api-report.md | 14 +- .../src/services/definitions/AuthService.ts | 2 + .../services/definitions/HttpAuthService.ts | 15 +- packages/backend-test-utils/api-report.md | 4 +- packages/backend-test-utils/package.json | 51 ++--- .../src/next/services/MockAuthService.test.ts | 28 +++ .../src/next/services/MockAuthService.ts | 6 - .../next/services/MockHttpAuthService.test.ts | 99 ++++++++- .../src/next/services/MockHttpAuthService.ts | 71 +++++-- .../src/next/services/mockServices.ts | 1 + yarn.lock | 1 + 16 files changed, 474 insertions(+), 158 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 93a889654a..78ff97a139 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -35,7 +35,6 @@ export type InternalBackstageCredentials = BackstageCredentials & { version: string; token?: string; - authMethod: 'token' | 'cookie' | 'none'; }; export function createCredentialsWithServicePrincipal( @@ -48,24 +47,23 @@ export function createCredentialsWithServicePrincipal( type: 'service', subject: sub, }, - authMethod: 'token', }; } export function createCredentialsWithUserPrincipal( sub: string, token: string, - authMethod: 'token' | 'cookie' = 'token', + expiresAt?: Date, ): InternalBackstageCredentials { return { $$type: '@backstage/BackstageCredentials', version: 'v1', token, + expiresAt, principal: { type: 'user', userEntityRef: sub, }, - authMethod, }; } @@ -76,7 +74,6 @@ export function createCredentialsWithNonePrincipal(): InternalBackstageCredentia principal: { type: 'none', }, - authMethod: 'none', }; } @@ -135,6 +132,7 @@ class DefaultAuthService implements AuthService { return createCredentialsWithUserPrincipal( identity.identity.userEntityRef, token, + this.#getJwtExpiration(token), ); } @@ -214,12 +212,15 @@ class DefaultAuthService implements AuthService { ); } + return { token, expiresAt: this.#getJwtExpiration(token) }; + } + + #getJwtExpiration(token: string) { const { exp } = decodeJwt(token); if (!exp) { throw new AuthenticationError('User token is missing expiration'); } - - return { token, expiresAt: new Date(exp * 1000) }; + return new Date(exp * 1000); } } diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index 1bd9d3cf2a..c261553685 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -18,6 +18,7 @@ import { AuthService, BackstageCredentials, BackstagePrincipalTypes, + BackstageUserPrincipal, DiscoveryService, HttpAuthService, coreServices, @@ -26,11 +27,8 @@ import { import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import { parse as parseCookie } from 'cookie'; import { Request, Response } from 'express'; -import { decodeJwt } from 'jose'; -import { - createCredentialsWithNonePrincipal, - toInternalBackstageCredentials, -} from '../auth/authServiceFactory'; + +const FIVE_MINUTES_MS = 5 * 60 * 1000; const BACKSTAGE_AUTH_COOKIE = 'backstage-auth'; @@ -41,54 +39,74 @@ function getTokenFromRequest(req: Request) { const matches = authHeader.match(/^Bearer[ ]+(\S+)$/i); const token = matches?.[1]; if (token) { - return { token, isCookie: false }; + return token; } } + return undefined; +} + +function getCookieFromRequest(req: Request) { const cookieHeader = req.headers.cookie; if (cookieHeader) { const cookies = parseCookie(cookieHeader); const token = cookies[BACKSTAGE_AUTH_COOKIE]; if (token) { - return { token, isCookie: true }; + return token; } } - return { token: undefined, isCookie: false }; + return undefined; } const credentialsSymbol = Symbol('backstage-credentials'); +const limitedCredentialsSymbol = Symbol('backstage-limited-credentials'); type RequestWithCredentials = Request & { [credentialsSymbol]?: Promise; + [limitedCredentialsSymbol]?: Promise; }; class DefaultHttpAuthService implements HttpAuthService { + readonly #auth: AuthService; + readonly #discovery: DiscoveryService; + readonly #pluginId: string; + constructor( - private readonly auth: AuthService, - private readonly discovery: DiscoveryService, - private readonly pluginId: string, - ) {} + auth: AuthService, + discovery: DiscoveryService, + pluginId: string, + ) { + this.#auth = auth; + this.#discovery = discovery; + this.#pluginId = pluginId; + } async #extractCredentialsFromRequest(req: Request) { - const { token, isCookie } = getTokenFromRequest(req); + const token = getTokenFromRequest(req); if (!token) { - return createCredentialsWithNonePrincipal(); + return await this.#auth.getNoneCredentials(); } - const credentials = toInternalBackstageCredentials( - await this.auth.authenticate(token), - ); - if (isCookie) { - if (credentials.principal.type !== 'user') { - throw new AuthenticationError( - 'Refusing to authenticate non-user principal with cookie auth', - ); - } - credentials.authMethod = 'cookie'; + return await this.#auth.authenticate(token); + } + + async #extractLimitedCredentialsFromRequest(req: Request) { + const token = getTokenFromRequest(req); + if (token) { + return await this.#auth.authenticate(token, { + allowLimitedAccess: true, + }); } - return credentials; + const cookie = getCookieFromRequest(req); + if (!cookie) { + return await this.#auth.getNoneCredentials(); + } + + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); } async #getCredentials(req: RequestWithCredentials) { @@ -96,73 +114,130 @@ class DefaultHttpAuthService implements HttpAuthService { this.#extractCredentialsFromRequest(req)); } + async #getLimitedCredentials(req: RequestWithCredentials) { + return (req[limitedCredentialsSymbol] ??= + this.#extractLimitedCredentialsFromRequest(req)); + } + async credentials( req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = toInternalBackstageCredentials( - await this.#getCredentials(req), - ); + // Limited and full credentials are treated as two separate cases, this lets + // us avoid internal dependencies between the AuthService and + // HttpAuthService implementations + const credentials = options?.allowLimitedAccess + ? await this.#getLimitedCredentials(req) + : await this.#getCredentials(req); - const allowedPrincipalTypes = options?.allow; - const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> = - options?.allowedAuthMethods ?? ['token']; - - if ( - credentials.authMethod !== 'none' && - !allowedAuthMethods.includes(credentials.authMethod) - ) { - throw new NotAllowedError( - `This endpoint does not allow the '${credentials.authMethod}' auth method`, - ); + const allowed = options?.allow; + if (!allowed) { + return credentials as any; } - if ( - allowedPrincipalTypes && - !allowedPrincipalTypes.includes(credentials.principal.type as TAllowed) - ) { - if (credentials.authMethod === 'none') { - throw new AuthenticationError(); + if (this.#auth.isPrincipal(credentials, 'none')) { + if (allowed.includes('none' as TAllowed)) { + return credentials as any; } + + throw new AuthenticationError('Missing credentials'); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowed.includes('user' as TAllowed)) { + return credentials as any; + } + throw new NotAllowedError( - `This endpoint does not allow '${credentials.principal.type}' credentials`, + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowed.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, ); } - return credentials as any; + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); } - async issueUserCookie(res: Response): Promise { - const credentials = await this.credentials(res.req, { allow: ['user'] }); + async issueUserCookie( + res: Response, + options?: { credentials?: BackstageCredentials }, + ): Promise<{ expiresAt: Date }> { + let credentials: BackstageCredentials; + if (options?.credentials) { + if (!this.#auth.isPrincipal(options.credentials, 'user')) { + throw new AuthenticationError( + 'Refused to issue cookie for non-user principal', + ); + } + credentials = options.credentials; + } else { + credentials = await this.credentials(res.req, { allow: ['user'] }); + } + + const existingExpiresAt = await this.#existingCookieExpiration(res.req); + if ( + existingExpiresAt && + existingExpiresAt.getTime() < Date.now() - FIVE_MINUTES_MS + ) { + return { expiresAt: existingExpiresAt }; + } + + const originHeader = res.req.headers.origin; + const origin = + !originHeader || originHeader === 'null' ? undefined : originHeader; // https://backstage.example.com/api/catalog - const externalBaseUrlStr = await this.discovery.getExternalBaseUrl( - this.pluginId, + const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl( + this.#pluginId, ); - const externalBaseUrl = new URL(externalBaseUrlStr); + const externalBaseUrl = new URL(origin ?? externalBaseUrlStr); - const { token } = toInternalBackstageCredentials(credentials); + const { token, expiresAt } = await this.#auth.getLimitedUserToken( + credentials, + ); if (!token) { throw new Error('User credentials is unexpectedly missing token'); } - // TODO: Proper refresh and expiration handling - const expires = decodeJwt(token).exp!; + const secure = + externalBaseUrl.protocol === 'https:' || + externalBaseUrl.hostname === 'localhost'; - // TODO: refresh this thing res.cookie(BACKSTAGE_AUTH_COOKIE, token, { domain: externalBaseUrl.hostname, httpOnly: true, - expires: new Date(expires * 1000), - path: externalBaseUrl.pathname, + expires: expiresAt, + secure, priority: 'high', - sameSite: 'lax', // TBD + sameSite: secure ? 'none' : 'lax', }); - throw new Error('Method not implemented.'); + return { expiresAt }; + } + + async #existingCookieExpiration(req: Request): Promise { + const existingCookie = getCookieFromRequest(req); + if (!existingCookie) { + return undefined; + } + + const existingCredentials = await this.#auth.authenticate(existingCookie, { + allowLimitedAccess: true, + }); + if (!this.#auth.isPrincipal(existingCredentials, 'user')) { + return undefined; + } + + return existingCredentials.expiresAt; } } diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts index b8430d398e..a5246c91b2 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts @@ -53,7 +53,10 @@ describe('createCredentialsBarrier', () => { .expect(401) .expect(res => expect(res.body).toMatchObject({ - error: { name: 'AuthenticationError', message: '' }, + error: { + name: 'AuthenticationError', + message: 'Missing credentials', + }, }), ); @@ -98,7 +101,7 @@ describe('createCredentialsBarrier', () => { .expect(200); }); - it('should allow exceptions to the default auth policy to be made', async () => { + it('should allow exceptions for unauthenticated access', async () => { const { app, barrier } = setup(); await request(app).get('/').send().expect(401); @@ -118,5 +121,55 @@ describe('createCredentialsBarrier', () => { await request(app).get('/other').send().expect(200); }); - // TODO: cookie auth + it('should allow exceptions for cookie access', async () => { + const { app, barrier } = setup(); + + await request(app).get('/').send().expect(401); + await request(app).get('/public').send().expect(401); + await request(app).get('/other').send().expect(401); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(401); + await request(app) + .get('/static') + .set('authorization', mockCredentials.user.header()) + .send() + .expect(200); + + barrier.addAuthPolicy({ allow: 'user-cookie', path: '/static' }); + + await request(app).get('/').send().expect(401); + await request(app).get('/static').send().expect(401); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(200); + await request(app) + .get('/static') + .set('authorization', mockCredentials.user.header()) + .send() + .expect(200); + + await request(app).get('/other').send().expect(401); + + // Unauthenticated access should take precedence + barrier.addAuthPolicy({ allow: 'unauthenticated', path: '/' }); + + await request(app).get('/').send().expect(200); + await request(app).get('/static').send().expect(200); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.cookie()) + .send() + .expect(200); + await request(app) + .get('/static') + .set('cookie', mockCredentials.limitedUser.invalidCookie()) + .send() + .expect(200); + await request(app).get('/other').send().expect(200); + }); }); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts index a512c5ac9e..a69fa5a804 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -76,7 +76,7 @@ export function createCredentialsBarrier(options: { httpAuth .credentials(req, { allow: ['user', 'service'], - allowedAuthMethods: allowsCookie ? ['token', 'cookie'] : ['token'], + allowLimitedAccess: allowsCookie, }) .then( () => next(), diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index fd44ca5b91..a46d553c5d 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -96,6 +96,7 @@ class AuthCompat implements AuthService { return createCredentialsWithUserPrincipal( identity.identity.userEntityRef, token, + this.#getJwtExpiration(token), ); } @@ -144,12 +145,15 @@ class AuthCompat implements AuthService { ); } + return { token, expiresAt: this.#getJwtExpiration(token) }; + } + + #getJwtExpiration(token: string) { const { exp } = decodeJwt(token); if (!exp) { throw new AuthenticationError('User token is missing expiration'); } - - return { token, expiresAt: new Date(exp * 1000) }; + return new Date(exp * 1000); } } @@ -174,7 +178,11 @@ type RequestWithCredentials = Request & { }; class HttpAuthCompat implements HttpAuthService { - constructor(private readonly auth: AuthService) {} + #auth: AuthService; + + constructor(auth: AuthService) { + this.#auth = auth; + } async #extractCredentialsFromRequest(req: Request) { const token = getTokenFromRequest(req); @@ -183,7 +191,7 @@ class HttpAuthCompat implements HttpAuthService { } const credentials = toInternalBackstageCredentials( - await this.auth.authenticate(token), + await this.#auth.authenticate(token), ); return credentials; @@ -198,39 +206,50 @@ class HttpAuthCompat implements HttpAuthService { req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { const credentials = toInternalBackstageCredentials( await this.#getCredentials(req), ); - const allowedPrincipalTypes = options?.allow; - const allowedAuthMethods: Array<'token' | 'cookie' | 'none'> = - options?.allowedAuthMethods ?? ['token']; + const allowed = options?.allow; + if (!allowed) { + return credentials as any; + } + + if (this.#auth.isPrincipal(credentials, 'none')) { + if (allowed.includes('none' as TAllowed)) { + return credentials as any; + } + + throw new AuthenticationError('Missing credentials'); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowed.includes('user' as TAllowed)) { + return credentials as any; + } - if ( - credentials.authMethod !== 'none' && - !allowedAuthMethods.includes(credentials.authMethod) - ) { throw new NotAllowedError( - `This endpoint does not allow the '${credentials.authMethod}' auth method`, + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowed.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, ); } - if ( - allowedPrincipalTypes && - !allowedPrincipalTypes.includes(credentials.principal.type as TAllowed) - ) { - throw new NotAllowedError( - `This endpoint does not allow '${credentials.principal.type}' credentials`, - ); - } - - return credentials as any; + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); } - async issueUserCookie(_res: Response): Promise {} + async issueUserCookie(_res: Response): Promise<{ expiresAt: Date }> { + return { expiresAt: new Date(Date.now() + 3600_000) }; + } } export class UserInfoCompat implements UserInfoService { diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index ec63ea9062..73c1d15cd1 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -38,6 +38,8 @@ export interface AuthService { expiresAt: Date; }>; // (undocumented) + getNoneCredentials(): Promise>; + // (undocumented) getOwnServiceCredentials(): Promise< BackstageCredentials >; @@ -119,6 +121,7 @@ export interface BackendPluginRegistrationPoints { // @public (undocumented) export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; principal: TPrincipal; }; @@ -311,11 +314,18 @@ export interface HttpAuthService { req: Request_2, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise>; // (undocumented) - issueUserCookie(res: Response_2): Promise; + issueUserCookie( + res: Response_2, + options?: { + credentials?: BackstageCredentials; + }, + ): Promise<{ + expiresAt: Date; + }>; } // @public (undocumented) diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index f9ff7edadc..2bcdc975a0 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -46,6 +46,8 @@ export type BackstageServicePrincipal = { export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + expiresAt?: Date; + principal: TPrincipal; }; diff --git a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts index 44696bd777..637109d1f0 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts @@ -15,7 +15,11 @@ */ import { Request, Response } from 'express'; -import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService'; +import { + BackstageCredentials, + BackstagePrincipalTypes, + BackstageUserPrincipal, +} from './AuthService'; /** @public */ export interface HttpAuthService { @@ -23,9 +27,14 @@ export interface HttpAuthService { req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise>; - issueUserCookie(res: Response): Promise; + issueUserCookie( + res: Response, + options?: { + credentials?: BackstageCredentials; + }, + ): Promise<{ expiresAt: Date }>; } diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 4991164013..ff3f50ef8c 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -54,9 +54,9 @@ export namespace mockCredentials { userEntityRef?: string, ): BackstageCredentials; export namespace limitedUser { - export function header(userEntityRef?: string): string; + export function cookie(userEntityRef?: string): string; // (undocumented) - export function invalidHeader(): string; + export function invalidCookie(): string; // (undocumented) export function invalidToken(): string; export function token(userEntityRef?: string): string; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index e2fc9dd3e6..801b7c125b 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,16 +1,30 @@ { "name": "@backstage/backend-test-utils", - "description": "Test helpers library for Backstage backends", "version": "0.3.0", - "main": "src/index.ts", - "types": "src/index.ts", + "description": "Test helpers library for Backstage backends", + "backstage": { + "role": "node-library" + }, "publishConfig": { "access": "public" }, + "keywords": [ + "backstage", + "test" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-test-utils" + }, + "license": "Apache-2.0", "exports": { ".": "./src/index.ts", "./package.json": "./package.json" }, + "main": "src/index.ts", + "types": "src/index.ts", "typesVersions": { "*": { "package.json": [ @@ -18,28 +32,17 @@ ] } }, - "backstage": { - "role": "node-library" - }, - "homepage": "https://backstage.io", - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "packages/backend-test-utils" - }, - "keywords": [ - "backstage", - "test" + "files": [ + "dist" ], - "license": "Apache-2.0", "scripts": { "build": "backstage-cli package build", + "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean", - "start": "backstage-cli package start" + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-app-api": "workspace:^", @@ -50,6 +53,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "better-sqlite3": "^9.0.0", + "cookie": "^0.6.0", "express": "^4.17.1", "fs-extra": "^11.0.0", "knex": "^3.0.0", @@ -60,15 +64,12 @@ "textextensions": "^5.16.0", "uuid": "^9.0.0" }, - "peerDependencies": { - "@types/jest": "*" - }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, - "files": [ - "dist" - ] + "peerDependencies": { + "@types/jest": "*" + } } diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts index 4343027aea..8741220a68 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts @@ -261,4 +261,32 @@ describe('MockAuthService', () => { `Refused to issue service token for credential type 'none'`, ); }); + + it('should issue limited user tokens', async () => { + await expect( + auth.getLimitedUserToken(mockCredentials.user()), + ).resolves.toEqual({ + token: mockCredentials.limitedUser.token(), + expiresAt: expect.any(Date), + }); + + await expect( + auth.getLimitedUserToken(mockCredentials.user('user:default/other')), + ).resolves.toEqual({ + token: mockCredentials.limitedUser.token('user:default/other'), + expiresAt: expect.any(Date), + }); + + await expect( + auth.getLimitedUserToken(mockCredentials.none() as any), + ).rejects.toThrow( + "Refused to issue limited user token for credential type 'none'", + ); + + await expect( + auth.getLimitedUserToken(mockCredentials.service() as any), + ).rejects.toThrow( + "Refused to issue limited user token for credential type 'service'", + ); + }); }); diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 4977c4df0e..c0e6461245 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -28,7 +28,6 @@ import { MOCK_USER_TOKEN, MOCK_USER_TOKEN_PREFIX, MOCK_INVALID_USER_TOKEN, - MOCK_USER_LIMITED_TOKEN, MOCK_USER_LIMITED_TOKEN_PREFIX, MOCK_INVALID_USER_LIMITED_TOKEN, MOCK_SERVICE_TOKEN, @@ -58,11 +57,6 @@ export class MockAuthService implements AuthService { switch (token) { case MOCK_USER_TOKEN: return mockCredentials.user(); - case MOCK_USER_LIMITED_TOKEN: - if (!options?.allowLimitedAccess) { - throw new AuthenticationError('Limited user token is not allowed'); - } - return mockCredentials.user(); case MOCK_SERVICE_TOKEN: return mockCredentials.service(); case MOCK_INVALID_USER_TOKEN: diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts index 922b3daaf4..b43f448b44 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts @@ -22,8 +22,11 @@ import { AuthenticationError } from '@backstage/errors'; describe('MockHttpAuthService', () => { const httpAuth = new MockHttpAuthService('test', mockCredentials.none()); - function makeAuthReq(header?: string) { - return { headers: { authorization: header } } as Request; + function makeAuthReq(authorization?: string) { + return { headers: { authorization } } as Request; + } + function makeCookieAuthReq(cookie?: string) { + return { headers: { cookie } } as Request; } it('should authenticate unauthenticated requests', async () => { @@ -68,6 +71,59 @@ describe('MockHttpAuthService', () => { ).resolves.toEqual(mockCredentials.user('user:default/other')); }); + it('should authenticate limited user requests', async () => { + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + ), + ).resolves.toEqual(mockCredentials.none()); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { allowLimitedAccess: true }, + ), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials(makeAuthReq(mockCredentials.user.header()), { + allowLimitedAccess: true, + }), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { + allow: ['user'], + }, + ), + ).rejects.toThrow('Missing credentials'); + + await expect( + httpAuth.credentials( + makeCookieAuthReq(mockCredentials.limitedUser.cookie()), + { + allow: ['none', 'service'], + allowLimitedAccess: true, + }, + ), + ).rejects.toThrow("This endpoint does not allow 'user' credentials"); + + await expect( + httpAuth.credentials( + makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`), + { allowLimitedAccess: true }, + ), + ).resolves.toEqual(mockCredentials.user()); + + await expect( + httpAuth.credentials( + makeAuthReq(`Bearer ${mockCredentials.limitedUser.token()}`), + ), + ).rejects.toThrow('Limited user token is not allowed'); + }); + it('should authenticate service requests', async () => { await expect( httpAuth.credentials(makeAuthReq(mockCredentials.service.header())), @@ -161,9 +217,42 @@ describe('MockHttpAuthService', () => { ).rejects.toThrow('Service token is invalid'); }); - it('does not implement .issueUserCookie', async () => { - await expect(httpAuth.issueUserCookie({} as any)).rejects.toThrow( - 'Not implemented', + it('should issue user cookie from request credentials', async () => { + const setHeader = jest.fn(); + + await expect( + httpAuth.issueUserCookie({ + req: makeAuthReq(mockCredentials.user.header()), + setHeader, + } as any), + ).resolves.toEqual({ + expiresAt: expect.any(Date), + }); + + expect(setHeader).toHaveBeenCalledWith( + 'Set-Cookie', + mockCredentials.limitedUser.cookie(), + ); + }); + + it('should issue user cookie from explicit credentials', async () => { + const setHeader = jest.fn(); + + await expect( + httpAuth.issueUserCookie( + { + req: makeAuthReq(mockCredentials.user.header()), + setHeader, + } as any, + { credentials: mockCredentials.user('user:default/other') }, + ), + ).resolves.toEqual({ + expiresAt: expect.any(Date), + }); + + expect(setHeader).toHaveBeenCalledWith( + 'Set-Cookie', + mockCredentials.limitedUser.cookie('user:default/other'), ); }); }); diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts index 9b133f996b..9a69620473 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts @@ -18,16 +18,18 @@ import { AuthService, BackstageCredentials, BackstagePrincipalTypes, + BackstageUserPrincipal, HttpAuthService, } from '@backstage/backend-plugin-api'; import { Request, Response } from 'express'; +import { parse as parseCookie } from 'cookie'; import { MockAuthService } from './MockAuthService'; +import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import { - AuthenticationError, - NotAllowedError, - NotImplementedError, -} from '@backstage/errors'; -import { mockCredentials } from './mockCredentials'; + MOCK_NONE_TOKEN, + MOCK_AUTH_COOKIE, + mockCredentials, +} from './mockCredentials'; // TODO: support mock cookie auth? export class MockHttpAuthService implements HttpAuthService { @@ -42,33 +44,52 @@ export class MockHttpAuthService implements HttpAuthService { this.#defaultCredentials = defaultCredentials; } - async #getCredentials(req: Request) { + async #getCredentials(req: Request, allowLimitedAccess: boolean) { const header = req.headers.authorization; - - if (header === mockCredentials.none.header()) { - return mockCredentials.none(); - } - const token = typeof header === 'string' ? header.match(/^Bearer[ ]+(\S+)$/i)?.[1] : undefined; - if (!token) { - return this.#defaultCredentials; + if (token) { + if (token === MOCK_NONE_TOKEN) { + return this.#auth.getNoneCredentials(); + } + + return await this.#auth.authenticate(token, { + allowLimitedAccess, + }); } - return await this.#auth.authenticate(token); + if (allowLimitedAccess) { + const cookieHeader = req.headers.cookie; + + if (cookieHeader) { + const cookies = parseCookie(cookieHeader); + const cookie = cookies[MOCK_AUTH_COOKIE]; + + if (cookie) { + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); + } + } + } + + return this.#defaultCredentials; } async credentials( req: Request, options?: { allow?: Array; - allowedAuthMethods?: Array<'token' | 'cookie'>; + allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = await this.#getCredentials(req); + const credentials = await this.#getCredentials( + req, + options?.allowLimitedAccess ?? false, + ); const allowedPrincipalTypes = options?.allow; if (!allowedPrincipalTypes) { @@ -80,7 +101,7 @@ export class MockHttpAuthService implements HttpAuthService { return credentials as any; } - throw new AuthenticationError(); + throw new AuthenticationError('Missing credentials'); } else if (this.#auth.isPrincipal(credentials, 'user')) { if (allowedPrincipalTypes.includes('user' as TAllowed)) { return credentials as any; @@ -104,7 +125,19 @@ export class MockHttpAuthService implements HttpAuthService { ); } - async issueUserCookie(_res: Response): Promise { - throw new NotImplementedError('Not implemented'); + async issueUserCookie( + res: Response, + options?: { credentials?: BackstageCredentials }, + ): Promise<{ expiresAt: Date }> { + const credentials = + options?.credentials ?? + (await this.credentials(res.req, { allow: ['user'] })); + + res.setHeader( + 'Set-Cookie', + mockCredentials.limitedUser.cookie(credentials.principal.userEntityRef), + ); + + return { expiresAt: new Date(Date.now() + 3600_000) }; } } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index f6d2d39167..3ff8a4e017 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -202,6 +202,7 @@ export namespace mockServices { }); export const mock = simpleMock(coreServices.auth, () => ({ authenticate: jest.fn(), + getNoneCredentials: jest.fn(), getOwnServiceCredentials: jest.fn(), isPrincipal: jest.fn() as any, getPluginRequestToken: jest.fn(), diff --git a/yarn.lock b/yarn.lock index eb1db50a2a..05604fd424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3483,6 +3483,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/supertest": ^2.0.8 better-sqlite3: ^9.0.0 + cookie: ^0.6.0 express: ^4.17.1 fs-extra: ^11.0.0 knex: ^3.0.0 From 7c8727ce057db1661209244b169f68a691c79697 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 12:07:29 +0100 Subject: [PATCH 409/483] backend-app-api: review fixes for cookie auth Signed-off-by: Patrik Oldsberg --- .../httpAuth/httpAuthServiceFactory.ts | 23 +++++++++++-------- .../src/next/services/MockAuthService.ts | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index c261553685..db36e5bf2a 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -59,6 +59,10 @@ function getCookieFromRequest(req: Request) { return undefined; } +function willExpireSoon(expiresAt: Date) { + return Date.now() + FIVE_MINUTES_MS > expiresAt.getTime(); +} + const credentialsSymbol = Symbol('backstage-credentials'); const limitedCredentialsSymbol = Symbol('backstage-limited-credentials'); @@ -100,13 +104,13 @@ class DefaultHttpAuthService implements HttpAuthService { } const cookie = getCookieFromRequest(req); - if (!cookie) { - return await this.#auth.getNoneCredentials(); + if (cookie) { + return await this.#auth.authenticate(cookie, { + allowLimitedAccess: true, + }); } - return await this.#auth.authenticate(cookie, { - allowLimitedAccess: true, - }); + return await this.#auth.getNoneCredentials(); } async #getCredentials(req: RequestWithCredentials) { @@ -171,6 +175,10 @@ class DefaultHttpAuthService implements HttpAuthService { res: Response, options?: { credentials?: BackstageCredentials }, ): Promise<{ expiresAt: Date }> { + if (res.headersSent) { + throw new Error('Failed to issue user cookie, headers were already sent'); + } + let credentials: BackstageCredentials; if (options?.credentials) { if (!this.#auth.isPrincipal(options.credentials, 'user')) { @@ -184,10 +192,7 @@ class DefaultHttpAuthService implements HttpAuthService { } const existingExpiresAt = await this.#existingCookieExpiration(res.req); - if ( - existingExpiresAt && - existingExpiresAt.getTime() < Date.now() - FIVE_MINUTES_MS - ) { + if (existingExpiresAt && !willExpireSoon(existingExpiresAt)) { return { expiresAt: existingExpiresAt }; } diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index c0e6461245..64dc92747a 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -181,7 +181,7 @@ export class MockAuthService implements AuthService { token: mockCredentials.limitedUser.token( credentials.principal.userEntityRef, ), - expiresAt: new Date(Date.now() + 3600), + expiresAt: new Date(Date.now() + 3600_000), }; } } From d3008408e8a110613d17d9ec647eeb8446b07171 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 13:49:48 +0100 Subject: [PATCH 410/483] kubernetes-backend: auth test fix Signed-off-by: Patrik Oldsberg --- plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index 9195551993..cfa89a6bfd 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -236,7 +236,7 @@ describe('resourcesRoutes', () => { .expect(401, { error: { name: 'AuthenticationError', - message: '', + message: 'Missing credentials', }, request: { method: 'POST', @@ -508,7 +508,7 @@ describe('resourcesRoutes', () => { .expect(401, { error: { name: 'AuthenticationError', - message: '', + message: 'Missing credentials', }, request: { method: 'POST', From 5d9c5ba0a6e7b7b6868fef3e4968afc00c215a0f Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Thu, 22 Feb 2024 12:06:47 +0100 Subject: [PATCH 411/483] feat: add createdAfter filtering to the Notifications Signed-off-by: Marek Libra --- .changeset/five-hats-accept.md | 6 ++ .../database/DatabaseNotificationsStore.ts | 4 ++ .../src/database/NotificationsStore.ts | 1 + .../src/service/router.ts | 7 +++ plugins/notifications/api-report.md | 1 + .../notifications/src/api/NotificationsApi.ts | 1 + .../src/api/NotificationsClient.ts | 4 +- .../NotificationsFilters.tsx | 60 ++++++++++--------- .../NotificationsPage/NotificationsPage.tsx | 23 +++++-- 9 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 .changeset/five-hats-accept.md diff --git a/.changeset/five-hats-accept.md b/.changeset/five-hats-accept.md new file mode 100644 index 0000000000..11ee4ff276 --- /dev/null +++ b/.changeset/five-hats-accept.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +The Notifications can be newly filtered based on the Created Date. diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 6870ece4be..8621072834 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -106,6 +106,10 @@ export class DatabaseNotificationsStore implements NotificationsStore { query.orderBy('created', options.sortOrder ?? 'desc'); } + if (options.createdAfter) { + query.where('created', '>=', options.createdAfter.valueOf()); + } + if (options.limit) { query.limit(options.limit); } diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 285f609446..0a7df92f03 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -31,6 +31,7 @@ export type NotificationGetOptions = { sortOrder?: 'asc' | 'desc'; read?: boolean; saved?: boolean; + createdAfter?: Date; }; /** @internal */ diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index d1bf281720..78d44dcd44 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -204,6 +204,13 @@ export async function createRouter( opts.read = false; // or keep undefined } + if (req.query.created_after) { + const sinceEpoch = Date.parse(req.query.created_after.toString()); + if (isNaN(sinceEpoch)) { + throw new InputError('Unexpected date format'); + } + opts.createdAfter = new Date(sinceEpoch); + } const notifications = await store.getNotifications(opts); res.send(notifications); diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 41b82f62ab..666daebdfe 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -21,6 +21,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + createdAfter?: Date; }; // @public (undocumented) diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 0125cc6a1e..4a1c792012 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -30,6 +30,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + createdAfter?: Date; }; /** @public */ diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 03f9c406a6..1013497b3d 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -54,7 +54,9 @@ export class NotificationsClient implements NotificationsApi { if (options?.read !== undefined) { queryString.append('read', options.read ? 'true' : 'false'); } - + if (options?.createdAfter !== undefined) { + queryString.append('created_after', options.createdAfter.toISOString()); + } const urlSegment = `?${queryString}`; return await this.request(urlSegment); diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index ac4b02307a..4645f46249 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -28,12 +28,13 @@ import { export type NotificationsFiltersProps = { unreadOnly?: boolean; onUnreadOnlyChanged: (checked: boolean | undefined) => void; - // createdAfter?: string; + createdAfter?: string; + onCreatedAfterChanged: (value: string) => void; + // sorting?: { // orderBy: GetNotificationsOrderByEnum; // orderByDirec: GetNotificationsOrderByDirecEnum; // }; - // onCreatedAfterChanged: (value: string) => void; // setSorting: ({ // orderBy, // orderByDirec, @@ -43,22 +44,22 @@ export type NotificationsFiltersProps = { // }) => void; }; -// export const CreatedAfterOptions: { -// [key: string]: { label: string; getDate: () => Date }; -// } = { -// last24h: { -// label: 'Last 24h', -// getDate: () => new Date(Date.now() - 24 * 3600 * 1000), -// }, -// lastWeek: { -// label: 'Last week', -// getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), -// }, -// all: { -// label: 'Any time', -// getDate: () => new Date(0), -// }, -// }; +export const CreatedAfterOptions: { + [key: string]: { label: string; getDate: () => Date }; +} = { + last24h: { + label: 'Last 24h', + getDate: () => new Date(Date.now() - 24 * 3600 * 1000), + }, + lastWeek: { + label: 'Last week', + getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000), + }, + all: { + label: 'Any time', + getDate: () => new Date(0), + }, +}; // export const SortByOptions: { // [key: string]: { @@ -108,20 +109,20 @@ export type NotificationsFiltersProps = { // }; export const NotificationsFilters = ({ - unreadOnly, - // createdAfter, // sorting, - // onCreatedAfterChanged, + // setSorting, + unreadOnly, onUnreadOnlyChanged, -}: // setSorting, -NotificationsFiltersProps) => { + createdAfter, + onCreatedAfterChanged, +}: NotificationsFiltersProps) => { // const sortBy = getSortBy(sorting); - // const handleOnCreatedAfterChanged = ( - // event: React.ChangeEvent<{ name?: string; value: unknown }>, - // ) => { - // onCreatedAfterChanged(event.target.value as string); - // }; + const handleOnCreatedAfterChanged = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + onCreatedAfterChanged(event.target.value as string); + }; const handleOnUnreadOnlyChanged = ( event: React.ChangeEvent<{ name?: string; value: unknown }>, @@ -169,7 +170,6 @@ NotificationsFiltersProps) => { - {/* TODO: extend BE to support following: @@ -190,6 +190,8 @@ NotificationsFiltersProps) => { + + {/* Sort by diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 43a402ac73..1f8b141608 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -20,11 +20,15 @@ import { PageWithHeader, ResponseErrorPanel, } from '@backstage/core-components'; -import { NotificationsTable } from '../NotificationsTable'; -import { useNotificationsApi } from '../../hooks'; import { Grid } from '@material-ui/core'; import { useSignal } from '@backstage/plugin-signals-react'; -import { NotificationsFilters } from '../NotificationsFilters'; + +import { NotificationsTable } from '../NotificationsTable'; +import { useNotificationsApi } from '../../hooks'; +import { + CreatedAfterOptions, + NotificationsFilters, +} from '../NotificationsFilters'; import { GetNotificationsOptions } from '../../api'; export const NotificationsPage = () => { @@ -32,6 +36,7 @@ export const NotificationsPage = () => { const { lastSignal } = useSignal('notifications'); const [unreadOnly, setUnreadOnly] = React.useState(true); const [containsText, setContainsText] = React.useState(); + const [createdAfter, setCreatedAfter] = React.useState('lastWeek'); const { error, value, retry, loading } = useNotificationsApi( // TODO: add pagination and other filters @@ -40,9 +45,15 @@ export const NotificationsPage = () => { if (unreadOnly !== undefined) { options.read = !unreadOnly; } + + const createdAfterDate = CreatedAfterOptions[createdAfter].getDate(); + if (createdAfterDate.valueOf() > 0) { + options.createdAfter = createdAfterDate; + } + return api.getNotifications(options); }, - [containsText, unreadOnly], + [containsText, unreadOnly, createdAfter], ); useEffect(() => { @@ -72,10 +83,10 @@ export const NotificationsPage = () => { From cacae47b1cd75c8a75e2620515a086bce226e837 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Thu, 22 Feb 2024 13:01:29 +0100 Subject: [PATCH 412/483] chore: add unit tests for the createdAfter filter of notifications Signed-off-by: Marek Libra --- .../DatabaseNotificationsStore.test.ts | 27 +++++++++++++++++++ .../src/api/NotificationsClient.test.ts | 17 +++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index c3e8ec95ed..e0af93a1d6 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -188,6 +188,33 @@ describe.each(databases.eachSupportedId())( expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id1); }); + + it('should filter notifications based on created date', async () => { + const id1 = uuid(); + const id2 = uuid(); + await insertNotification({ + id: id1, + ...testNotification, + created: new Date(Date.now() - 1 * 60 * 60 * 1000 /* an hour ago */), + }); + await insertNotification({ + id: id2, + ...testNotification, + payload: { + severity: 'normal', + title: 'Please find me', + }, + created: new Date() /* now */, + }); + await insertNotification({ id: uuid(), ...otherUserNotification }); + + const notifications = await storage.getNotifications({ + user, + createdAfter: new Date(Date.now() - 5 * 60 * 1000 /* 5mins */), + }); + expect(notifications.length).toBe(1); + expect(notifications.at(0)?.id).toEqual(id2); + }); }); describe('getStatus', () => { diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index 09b5e3647c..daf9e71232 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -60,7 +60,7 @@ describe('NotificationsClient', () => { server.use( rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { expect(req.url.search).toBe( - '?limit=10&offset=0&search=find+me&read=true', + '?limit=10&offset=0&search=find+me&read=true&created_after=1970-01-01T00%3A00%3A00.005Z', ); return res(ctx.json(expectedResp)); }), @@ -70,6 +70,21 @@ describe('NotificationsClient', () => { offset: 0, search: 'find me', read: true, + createdAfter: new Date(5), + }); + expect(response).toEqual(expectedResp); + }); + + it('should omit unselected fetch options', async () => { + server.use( + rest.get(`${mockBaseUrl}/`, (req, res, ctx) => { + expect(req.url.search).toBe('?limit=10'); + return res(ctx.json(expectedResp)); + }), + ); + const response = await client.getNotifications({ + limit: 10, + // do not put more options here }); expect(response).toEqual(expectedResp); }); From 672f8e3b423814f536e4eaf835a140a3e2074135 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Thu, 22 Feb 2024 15:53:34 +0100 Subject: [PATCH 413/483] chore: filter timestamp on different DB engines Signed-off-by: Marek Libra --- .../src/database/DatabaseNotificationsStore.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 8621072834..9b654cfd04 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -98,6 +98,9 @@ export class DatabaseNotificationsStore implements NotificationsStore { options: NotificationGetOptions | NotificationModifyOptions, ) => { const { user } = options; + const isSQLite = this.db.client.config.client.includes('sqlite3'); + // const isPsql = this.db.client.config.client.includes('pg'); + const query = this.db('notification').where('user', user); if (options.sort !== undefined && options.sort !== null) { @@ -107,7 +110,19 @@ export class DatabaseNotificationsStore implements NotificationsStore { } if (options.createdAfter) { - query.where('created', '>=', options.createdAfter.valueOf()); + if (isSQLite) { + query.where( + 'notification.created', + '>=', + options.createdAfter.valueOf(), + ); + } else { + query.where( + 'notification.created', + '>=', + options.createdAfter.toISOString(), + ); + } } if (options.limit) { From ff7e12632dde109b909948ed693f4da36e91eab5 Mon Sep 17 00:00:00 2001 From: rui ma Date: Mon, 8 Jan 2024 23:55:00 +0800 Subject: [PATCH 414/483] feat: support i18n for core component Signed-off-by: rui ma --- .changeset/flat-badgers-attack.md | 5 + packages/core-components/api-report-alpha.md | 73 ++++++++++ packages/core-components/package.json | 4 + packages/core-components/src/alpha.ts | 16 +++ .../components/AlertDisplay/AlertDisplay.tsx | 14 +- .../components/AutoLogout/Autologout.test.tsx | 50 +++---- .../AutoLogout/StillTherePrompt.tsx | 15 +- .../CopyTextButton/CopyTextButton.tsx | 5 +- .../MissingAnnotationEmptyState.tsx | 26 ++-- .../src/components/Link/Link.tsx | 5 +- .../LoginRequestListItem.tsx | 5 +- .../OAuthRequestDialog/OAuthRequestDialog.tsx | 13 +- .../SimpleStepper/SimpleStepperFooter.tsx | 82 ++++++----- .../SupportButton/SupportButton.tsx | 7 +- .../src/components/Table/Filters.tsx | 7 +- .../src/hooks/useSupportConfig.ts | 38 ++--- .../layout/ErrorBoundary/ErrorBoundary.tsx | 13 +- .../src/layout/ErrorPage/ErrorPage.tsx | 17 ++- .../ProxiedSignInPage/ProxiedSignInPage.tsx | 10 +- .../src/layout/Sidebar/Bar.tsx | 5 +- .../src/layout/SignInPage/SignInPage.tsx | 5 +- .../src/layout/SignInPage/commonProvider.tsx | 7 +- .../src/layout/SignInPage/customProvider.tsx | 17 ++- packages/core-components/src/translation.ts | 130 ++++++++++++++++++ .../components/EntityBadgesDialog.test.tsx | 4 +- .../BitriseBuildsComponent.test.tsx | 6 +- plugins/git-release-manager/package.json | 1 + .../src/features/Features.test.tsx | 36 +++-- .../src/features/Info/Info.test.tsx | 5 +- .../src/features/Patch/PatchBody.test.tsx | 49 +++++-- .../GoCdBuildsComponent.test.tsx | 4 +- .../components/VisitList/VisitList.test.tsx | 112 +++++++-------- .../Pods/FixDialog/FixDialog.test.tsx | 15 +- .../TechDocsSearchResultListItem.test.tsx | 8 +- .../src/components/TodoList/TodoList.test.tsx | 4 +- .../EntityVaultCard/EntityVaultCard.test.tsx | 9 +- yarn.lock | 1 + 37 files changed, 575 insertions(+), 248 deletions(-) create mode 100644 .changeset/flat-badgers-attack.md create mode 100644 packages/core-components/api-report-alpha.md create mode 100644 packages/core-components/src/alpha.ts create mode 100644 packages/core-components/src/translation.ts diff --git a/.changeset/flat-badgers-attack.md b/.changeset/flat-badgers-attack.md new file mode 100644 index 0000000000..69250d0ded --- /dev/null +++ b/.changeset/flat-badgers-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Support i18n for core components diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md new file mode 100644 index 0000000000..a51e590812 --- /dev/null +++ b/packages/core-components/api-report-alpha.md @@ -0,0 +1,73 @@ +## API Report File for "@backstage/core-components" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export const coreComponentsTranslationRef: TranslationRef< + 'core-components', + { + readonly 'link.openNewWindow': 'Opens in a new window'; + readonly 'table.filter.title': 'Filters'; + readonly 'table.filter.clearAll': 'Clear all'; + readonly 'signIn.title': 'Sign In'; + readonly 'signIn.loginFailed': 'Login failed'; + readonly 'signIn.customProvider.title': 'Custom User'; + readonly 'signIn.customProvider.subtitle2': 'This selection will not be stored.'; + readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.'; + readonly 'signIn.customProvider.userId': 'User ID'; + readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; + readonly 'signIn.customProvider.continue': 'Continue'; + readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; + readonly 'signIn.guestProvider.title': 'Guest'; + readonly 'signIn.guestProvider.description': 'You will not have a verified identity, meaning some features might be unavailable.'; + readonly 'signIn.guestProvider.enter': 'Enter'; + readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.'; + readonly 'sidebar.shipToContent': 'Skip to content'; + readonly 'sidebar.starredIntroText': 'Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!'; + readonly 'sidebar.recentlyViewedIntroText': 'And your recently viewed plugins will pop up here!'; + readonly 'sidebar.dismiss': 'Dismiss'; + readonly 'copyTextButton.tooltipText': 'Text copied to clipboard'; + readonly 'simpleStepper.finish': 'Finish'; + readonly 'simpleStepper.reset': 'Reset'; + readonly 'simpleStepper.next': 'Next'; + readonly 'simpleStepper.skip': 'Skip'; + readonly 'simpleStepper.back': 'Back'; + readonly 'errorPage.title': 'Looks like someone dropped the mic!'; + readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}'; + readonly 'errorPage.goBack': 'Go back'; + readonly 'errorPage.orPlease': '... or please '; + readonly 'errorPage.contactSupport': 'contact support'; + readonly 'errorPage.isBug': 'if you think this is a bug.'; + readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; + readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; + readonly 'emptyState.missingAnnotation.readMore': 'Read more'; + readonly 'emptyState.missingAnnotation.descriptionPrefix_one': 'The annotation '; + readonly 'emptyState.missingAnnotation.descriptionPrefix_other': 'The annotations '; + readonly 'emptyState.missingAnnotation.descriptionSuffix_one': ' is missing. You need to add the annotation to your component if you want to enable this tool.'; + readonly 'emptyState.missingAnnotation.descriptionSuffix_other': ' are missing. You need to add the annotations to your component if you want to enable this tool.'; + readonly 'supportConfig.title': 'Support Not Configured'; + readonly 'supportConfig.links.title': 'Add `app.support` config key'; + readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; + readonly 'oauthRequestDialog.title': 'Login Required'; + readonly 'oauthRequestDialog.authRedirectTitle': 'This will trigger a http redirect to OAuth Login.'; + readonly 'oauthRequestDialog.login': 'Log in'; + readonly 'oauthRequestDialog.rejectAll': 'Reject All'; + readonly 'supportButton.title': 'Support'; + readonly 'supportButton.close': 'Close'; + readonly 'alertDisplay.message_one': '({{ num }} older message)'; + readonly 'alertDisplay.message_other': '({{ num }} older messages)'; + readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; + readonly 'autoLogout.stillTherePrompt.description': 'You are about to be disconnected in'; + readonly 'autoLogout.stillTherePrompt.second_one': 'second'; + readonly 'autoLogout.stillTherePrompt.second_other': 'seconds'; + readonly 'autoLogout.stillTherePrompt.descriptionSuffix': 'Are you still there?'; + readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; + } +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 73de44d5ed..04edbeb212 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -22,11 +22,15 @@ "types": "src/index.ts", "exports": { ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { + "alpha": [ + "src/alpha.ts" + ], "testUtils": [ "src/testUtils.ts" ], diff --git a/packages/core-components/src/alpha.ts b/packages/core-components/src/alpha.ts new file mode 100644 index 0000000000..e1f7678bae --- /dev/null +++ b/packages/core-components/src/alpha.ts @@ -0,0 +1,16 @@ +/* + * 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 './translation'; diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx index b1c8273749..19527f76c2 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ import { alertApiRef, AlertMessage, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import IconButton from '@material-ui/core/IconButton'; import Snackbar from '@material-ui/core/Snackbar'; import Typography from '@material-ui/core/Typography'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; -import pluralize from 'pluralize'; import React, { useEffect, useState } from 'react'; +import { coreComponentsTranslationRef } from '../../translation'; /** * Properties for {@link AlertDisplay} @@ -63,6 +64,7 @@ export type AlertDisplayProps = { export function AlertDisplay(props: AlertDisplayProps) { const [messages, setMessages] = useState>([]); const alertApi = useApi(alertApiRef); + const { t } = useTranslationRef(coreComponentsTranslationRef); const { anchorOrigin = { vertical: 'top', horizontal: 'center' }, @@ -121,10 +123,12 @@ export function AlertDisplay(props: AlertDisplayProps) { {String(firstMessage.message)} {messages.length > 1 && ( - {` (${messages.length - 1} older ${pluralize( - 'message', - messages.length - 1, - )})`} + + {t('alertDisplay.message', { + num: String(messages.length - 1), + count: messages.length - 1, + })} + )} diff --git a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx index 0d414524a5..164ad716d2 100644 --- a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx +++ b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx @@ -18,16 +18,18 @@ import { createMocks } from 'react-idle-timer'; import { MessageChannel } from 'worker_threads'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { AutoLogout } from './AutoLogout'; -import { cleanup, render } from '@testing-library/react'; +import { cleanup } from '@testing-library/react'; // Mock the signOut function of identityApiRef const mockSignOut = jest.fn(); -const mockIdentityApi = { signOut: mockSignOut }; - +const mockIdentityApi = { + signOut: mockSignOut, + getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }), +}; const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]); describe('AutoLogout', () => { @@ -44,27 +46,29 @@ describe('AutoLogout', () => { }); it('should throw error if idleTimeoutMinutes is smaller than promptBeforeSeconds', async () => { - expect(() => - render( - - - , - ), - ).toThrow(); + await expect( + async () => + await renderInTestApp( + + + , + ), + ).rejects.toThrow(); }); it('should throw error if idleTimeoutMinutes is smaller than 30 seconds', async () => { - expect(() => - render( - - -
Test Child
-
, - ), - ).toThrow(); + await expect( + async () => + await renderInTestApp( + + +
Test Child
+
, + ), + ).rejects.toThrow(); }); }); diff --git a/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx b/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx index e0cfc8dd56..c704cf9910 100644 --- a/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx +++ b/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx @@ -22,6 +22,8 @@ import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import React, { useEffect } from 'react'; import { IIdleTimer } from 'react-idle-timer'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export interface StillTherePromptProps { idleTimer: IIdleTimer; @@ -41,6 +43,7 @@ export const StillTherePrompt = (props: StillTherePromptProps) => { remainingTime, setRemainingTime, } = props; + const { t } = useTranslationRef(coreComponentsTranslationRef); useEffect(() => { const interval = setInterval(() => { @@ -61,18 +64,18 @@ export const StillTherePrompt = (props: StillTherePromptProps) => { remainingTime - promptTimeoutMillis / 1000, 0, ); - const seconds = timeTillPrompt > 1 ? 'seconds' : 'second'; return ( - Logging out due to inactivity + {t('autoLogout.stillTherePrompt.title')} - You are about to be disconnected in{' '} + {t('autoLogout.stillTherePrompt.description')}{' '} - {Math.ceil(remainingTime / 1000)} {seconds} + {Math.ceil(remainingTime / 1000)}{' '} + {t('autoLogout.stillTherePrompt.second', { count: timeTillPrompt })} - . Are you still there? + . {t('autoLogout.stillTherePrompt.descriptionSuffix')} @@ -82,7 +85,7 @@ export const StillTherePrompt = (props: StillTherePromptProps) => { variant="contained" size="small" > - Yes! Don't log me out + {t('autoLogout.stillTherePrompt.buttonText')} diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx index bd900a931d..91b046354a 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx @@ -20,6 +20,8 @@ import Tooltip from '@material-ui/core/Tooltip'; import CopyIcon from '@material-ui/icons/FileCopy'; import React, { MouseEventHandler, useEffect, useState } from 'react'; import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** * Properties for {@link CopyTextButton} @@ -78,10 +80,11 @@ export interface CopyTextButtonProps { * ``` */ export function CopyTextButton(props: CopyTextButtonProps) { + const { t } = useTranslationRef(coreComponentsTranslationRef); const { text, tooltipDelay = 1000, - tooltipText = 'Text copied to clipboard', + tooltipText = t('copyTextButton.tooltipText'), 'aria-label': ariaLabel = 'Copy text', } = props; const errorApi = useApi(errorApiRef); diff --git a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx index 81b4714531..8c3f8c186f 100644 --- a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -23,6 +23,8 @@ import React from 'react'; import { CodeSnippet } from '../CodeSnippet'; import { Link } from '../Link'; import { EmptyState } from './EmptyState'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { coreComponentsTranslationRef } from '../../translation'; const COMPONENT_YAML_TEMPLATE = `apiVersion: backstage.io/v1alpha1 kind: Component @@ -76,11 +78,13 @@ function generateComponentYaml(annotations: string[]) { return COMPONENT_YAML_TEMPLATE.replace(ANNOTATION_YAML, annotationYaml); } -function generateDescription(annotations: string[]) { - const isSingular = annotations.length <= 1; +function useGenerateDescription(annotations: string[]) { + const { t } = useTranslationRef(coreComponentsTranslationRef); return ( <> - The {isSingular ? 'annotation' : 'annotations'}{' '} + {t('emptyState.missingAnnotation.descriptionPrefix', { + count: annotations.length, + })} {annotations .map(ann => {ann}) .reduce((prev, curr) => ( @@ -88,9 +92,9 @@ function generateDescription(annotations: string[]) { {prev}, {curr} ))}{' '} - {isSingular ? 'is' : 'are'} missing. You need to add the{' '} - {isSingular ? 'annotation' : 'annotations'} to your component if you want - to enable this tool. + {t('emptyState.missingAnnotation.descriptionSuffix', { + count: annotations.length, + })} ); } @@ -106,17 +110,17 @@ export function MissingAnnotationEmptyState(props: Props) { readMoreUrl || 'https://backstage.io/docs/features/software-catalog/well-known-annotations'; const classes = useStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); return ( - Add the annotation to your component YAML as shown in the - highlighted example below: + {t('emptyState.missingAnnotation.actionTitle')} } diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index e957390da8..b426402ba9 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; // eslint-disable-next-line no-restricted-imports import MaterialLink, { LinkProps as MaterialLinkProps, @@ -29,6 +30,7 @@ import { LinkProps as RouterLinkProps, Route, } from 'react-router-dom'; +import { coreComponentsTranslationRef } from '../../translation'; export function isReactRouterBeta(): boolean { const [obj] = createRoutesFromChildren(} />); @@ -161,6 +163,7 @@ export const Link = React.forwardRef( ({ onClick, noTrack, ...props }, ref) => { const classes = useStyles(); const analytics = useAnalytics(); + const { t } = useTranslationRef(coreComponentsTranslationRef); // Adding the base path to URLs breaks react-router v6 stable, so we only // do it for beta. The react router version won't change at runtime so it is @@ -199,7 +202,7 @@ export const Link = React.forwardRef( > {props.children} - , Opens in a new window + {`, ${t('link.openNewWindow')}`} ) : ( diff --git a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index 782624362d..375ea66953 100644 --- a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -23,6 +23,8 @@ import Button from '@material-ui/core/Button'; import React, { useState } from 'react'; import { isError } from '@backstage/errors'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type LoginRequestListItemClassKey = 'root'; @@ -44,6 +46,7 @@ type RowProps = { const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { const classes = useItemStyles(); const [error, setError] = useState(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const handleContinue = async () => { setBusy(true); @@ -68,7 +71,7 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { secondary={error && {error}} /> ); diff --git a/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx index 7c17e74e4a..2257b776d9 100644 --- a/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx +++ b/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx @@ -30,6 +30,8 @@ import { oauthRequestApiRef, } from '@backstage/core-plugin-api'; import Typography from '@material-ui/core/Typography'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type OAuthRequestDialogClassKey = | 'dialog' @@ -63,6 +65,7 @@ export function OAuthRequestDialog(_props: {}) { const [busy, setBusy] = useState(false); const oauthRequestApi = useApi(oauthRequestApiRef); const configApi = useApi(configApiRef); + const { t } = useTranslationRef(coreComponentsTranslationRef); const authRedirect = configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false; @@ -94,12 +97,10 @@ export function OAuthRequestDialog(_props: {}) { variant="h1" variantMapping={{ h1: 'span' }} > - Login Required + {t('oauthRequestDialog.title')} {authRedirect ? ( - - This will trigger a http redirect to OAuth Login. - + {t('oauthRequestDialog.authRedirectTitle')} ) : null} @@ -118,7 +119,9 @@ export function OAuthRequestDialog(_props: {}) { - + ); diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx index 224b66533a..8db3ac8d72 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepperFooter.tsx @@ -20,6 +20,8 @@ import React, { PropsWithChildren, ReactNode, useContext } from 'react'; import { VerticalStepperContext } from './SimpleStepper'; import { StepActions } from './types'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type SimpleStepperFooterClassKey = 'root'; @@ -55,9 +57,12 @@ interface BackBtnProps extends CommonBtnProps { disabled?: boolean; stepIndex: number; } -export const RestartBtn = ({ text, handleClick }: RestartBtnProps) => ( - -); +export const RestartBtn = ({ text, handleClick }: RestartBtnProps) => { + const { t } = useTranslationRef(coreComponentsTranslationRef); + return ( + + ); +}; const NextBtn = ({ text, @@ -65,39 +70,48 @@ const NextBtn = ({ disabled, last, stepIndex, -}: NextBtnProps) => ( - -); +}: NextBtnProps) => { + const { t } = useTranslationRef(coreComponentsTranslationRef); + return ( + + ); +}; -const SkipBtn = ({ text, handleClick, disabled, stepIndex }: SkipBtnProps) => ( - -); +const SkipBtn = ({ text, handleClick, disabled, stepIndex }: SkipBtnProps) => { + const { t } = useTranslationRef(coreComponentsTranslationRef); + return ( + + ); +}; -const BackBtn = ({ text, handleClick, disabled, stepIndex }: BackBtnProps) => ( - -); +const BackBtn = ({ text, handleClick, disabled, stepIndex }: BackBtnProps) => { + const { t } = useTranslationRef(coreComponentsTranslationRef); + return ( + + ); +}; export type SimpleStepperFooterProps = { actions?: StepActions; diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 726500c77e..623eda3098 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -31,6 +31,8 @@ import React, { MouseEventHandler, useState } from 'react'; import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; import { HelpIcon } from '../../icons'; import { Link } from '../Link'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type SupportButtonProps = { title?: string; @@ -85,6 +87,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { }; export function SupportButton(props: SupportButtonProps) { + const { t } = useTranslationRef(coreComponentsTranslationRef); const { title, items, children } = props; const { items: configItems } = useSupportConfig(); @@ -125,7 +128,7 @@ export function SupportButton(props: SupportButtonProps) { onClick={onClickHandler} startIcon={} > - Support + {t('supportButton.title')} )} @@ -171,7 +174,7 @@ export function SupportButton(props: SupportButtonProps) { onClick={popoverCloseHandler} aria-label="Close" > - Close + {t('supportButton.close')} diff --git a/packages/core-components/src/components/Table/Filters.tsx b/packages/core-components/src/components/Table/Filters.tsx index 568b22b933..999676f4d9 100644 --- a/packages/core-components/src/components/Table/Filters.tsx +++ b/packages/core-components/src/components/Table/Filters.tsx @@ -21,6 +21,8 @@ import React, { useEffect, useState } from 'react'; import { Select } from '../Select'; import { SelectProps } from '../Select/Select'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type TableFiltersClassKey = 'root' | 'value' | 'heder' | 'filters'; @@ -76,6 +78,7 @@ export const Filters = (props: Props) => { const classes = useFilterStyles(); const { onChangeFilters } = props; + const { t } = useTranslationRef(coreComponentsTranslationRef); const [selectedFilters, setSelectedFilters] = useState({ ...props.selectedFilters, @@ -96,9 +99,9 @@ export const Filters = (props: Props) => { return ( - Filters + {t('table.filter.title')} diff --git a/packages/core-components/src/hooks/useSupportConfig.ts b/packages/core-components/src/hooks/useSupportConfig.ts index e80eb457a1..49cc44c0f7 100644 --- a/packages/core-components/src/hooks/useSupportConfig.ts +++ b/packages/core-components/src/hooks/useSupportConfig.ts @@ -15,6 +15,8 @@ */ import { useApiHolder, configApiRef } from '@backstage/core-plugin-api'; +import { coreComponentsTranslationRef } from '../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export type SupportItemLink = { url: string; @@ -32,30 +34,34 @@ export type SupportConfig = { items: SupportItem[]; }; -const DEFAULT_SUPPORT_CONFIG: SupportConfig = { - url: 'https://github.com/backstage/backstage/issues', - items: [ - { - title: 'Support Not Configured', - icon: 'warning', - links: [ - { - // TODO: Update to dedicated support page on backstage.io/docs - title: 'Add `app.support` config key', - url: 'https://github.com/backstage/backstage/blob/master/app-config.yaml', - }, - ], - }, - ], +const useDefaultSupportConfig = () => { + const { t } = useTranslationRef(coreComponentsTranslationRef); + return { + url: 'https://github.com/backstage/backstage/issues', + items: [ + { + title: t('supportConfig.title'), + icon: 'warning', + links: [ + { + // TODO: Update to dedicated support page on backstage.io/docs + title: t('supportConfig.links.title'), + url: 'https://github.com/backstage/backstage/blob/master/app-config.yaml', + }, + ], + }, + ], + }; }; export function useSupportConfig(): SupportConfig { const apiHolder = useApiHolder(); const config = apiHolder.get(configApiRef); const supportConfig = config?.getOptionalConfig('app.support'); + const defaultSupportConfig = useDefaultSupportConfig(); if (!supportConfig) { - return DEFAULT_SUPPORT_CONFIG; + return defaultSupportConfig; } return { diff --git a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx index 0ecaedfc19..8f746882a1 100644 --- a/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/core-components/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -18,6 +18,8 @@ import Typography from '@material-ui/core/Typography'; import React, { ComponentClass, Component, ErrorInfo } from 'react'; import { LinkButton } from '../../components/LinkButton'; import { ErrorPanel } from '../../components/ErrorPanel'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type SlackChannel = { name: string; @@ -37,14 +39,21 @@ type State = { const SlackLink = (props: { slackChannel?: string | SlackChannel }) => { const { slackChannel } = props; + const { t } = useTranslationRef(coreComponentsTranslationRef); if (!slackChannel) { return null; } else if (typeof slackChannel === 'string') { - return Please contact {slackChannel} for help.; + return ( + {t('errorBoundary.title', { slackChannel })} + ); } else if (!slackChannel.href) { return ( - Please contact {slackChannel.name} for help. + + {t('errorBoundary.title', { + slackChannel: slackChannel.name, + })} + ); } diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 131df3da3a..f80161ff3c 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -23,6 +23,8 @@ import { Link } from '../../components/Link'; import { useSupportConfig } from '../../hooks'; import { MicDrop } from './MicDrop'; import { StackDetails } from './StackDetails'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; interface IErrorPageProps { status?: string; @@ -68,6 +70,7 @@ export function ErrorPage(props: IErrorPageProps) { const classes = useStyles(); const navigate = useNavigate(); const support = useSupportConfig(); + const { t } = useTranslationRef(coreComponentsTranslationRef); return ( @@ -77,21 +80,23 @@ export function ErrorPage(props: IErrorPageProps) { variant="body1" className={classes.subtitle} > - ERROR {status}: {statusMessage} + {t('errorPage.subtitle', { status: status || '', statusMessage })} {additionalInfo} - Looks like someone dropped the mic! + {t('errorPage.title')} navigate(-1)}> - Go back + {t('errorPage.goBack')} - ... or please{' '} - contact support if you - think this is a bug. + {t('errorPage.orPlease')} + + {t('errorPage.contactSupport')} + + {t('errorPage.isBug')} {stack && } diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx index ea095cd8ab..98fca7c6ef 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx @@ -24,6 +24,8 @@ import { useAsync, useMountEffect } from '@react-hookz/web'; import { ErrorPanel } from '../../components/ErrorPanel'; import { Progress } from '../../components/Progress'; import { ProxiedSignInIdentity } from './ProxiedSignInIdentity'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** * Props for {@link ProxiedSignInPage}. @@ -61,6 +63,7 @@ export type ProxiedSignInPageProps = SignInPageProps & { */ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => { const discoveryApi = useApi(discoveryApiRef); + const { t } = useTranslationRef(coreComponentsTranslationRef); const [{ status, error }, { execute }] = useAsync(async () => { const identity = new ProxiedSignInIdentity({ @@ -79,12 +82,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => { if (status === 'loading') { return ; } else if (error) { - return ( - - ); + return ; } return null; diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 550c614522..f8c6036158 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -34,6 +34,8 @@ import { MobileSidebar } from './MobileSidebar'; import { useContent } from './Page'; import { SidebarOpenStateProvider } from './SidebarOpenStateContext'; import { useSidebarPinState } from './SidebarPinStateContext'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { coreComponentsTranslationRef } from '../../translation'; /** @public */ export type SidebarClassKey = 'drawer' | 'drawerOpen'; @@ -250,6 +252,7 @@ function A11ySkipSidebar() { const { sidebarConfig } = useContext(SidebarConfigContext); const { focusContent, contentRef } = useContent(); const classes = useStyles({ sidebarConfig }); + const { t } = useTranslationRef(coreComponentsTranslationRef); if (!contentRef?.current) { return null; @@ -260,7 +263,7 @@ function A11ySkipSidebar() { variant="contained" className={classnames(classes.visuallyHidden)} > - Skip to content + {t('sidebar.shipToContent')} ); } diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index 6092f2862f..95d96b701a 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -35,6 +35,8 @@ import { Page } from '../Page'; import { getSignInProviders, useSignInProviders } from './providers'; import { GridItem, useStyles } from './styles'; import { IdentityProviders, SignInProviderConfig } from './types'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type MultiSignInPageProps = SignInPageProps & { providers: IdentityProviders; @@ -95,6 +97,7 @@ export const SingleSignInPage = ({ const classes = useStyles(); const authApi = useApi(provider.apiRef); const configApi = useApi(configApiRef); + const { t } = useTranslationRef(coreComponentsTranslationRef); const [error, setError] = useState(); @@ -174,7 +177,7 @@ export const SingleSignInPage = ({ login({ showPopup: true }); }} > - Sign In + {t('signIn.title')} } > diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 1827981e6f..07a9d9c98e 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -28,6 +28,8 @@ import { useApi, errorApiRef } from '@backstage/core-plugin-api'; import { GridItem } from './styles'; import { ForwardedError } from '@backstage/errors'; import { UserIdentity } from './UserIdentity'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const Component: ProviderComponent = ({ config, @@ -38,6 +40,7 @@ const Component: ProviderComponent = ({ const { apiRef, title, message } = config as SignInProviderConfig; const authApi = useApi(apiRef); const errorApi = useApi(errorApiRef); + const { t } = useTranslationRef(coreComponentsTranslationRef); const handleLogin = async () => { try { @@ -63,7 +66,7 @@ const Component: ProviderComponent = ({ ); } catch (error) { onSignInFailure(); - errorApi.post(new ForwardedError('Login failed', error)); + errorApi.post(new ForwardedError(t('signIn.loginFailed'), error)); } }; @@ -74,7 +77,7 @@ const Component: ProviderComponent = ({ title={title} actions={ } > diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index ad02715447..ebab5dce5c 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -27,6 +27,8 @@ import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { GridItem } from './styles'; import { UserIdentity } from './UserIdentity'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; @@ -63,6 +65,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => { const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { const classes = useFormStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const { register, handleSubmit, formState } = useForm({ mode: 'onChange', }); @@ -84,18 +87,18 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { return ( - + - Enter your own User ID and credentials. + {t('signIn.customProvider.subtitle')}
- This selection will not be stored. + {t('signIn.customProvider.subtitle2')}
@@ -111,10 +114,10 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { validate: token => !token || ID_TOKEN_REGEX.test(token) || - 'Token is not a valid OpenID Connect JWT Token', + t('signIn.customProvider.tokenInvalid'), }), )} - label="ID Token (optional)" + label={t('signIn.customProvider.idToken')} margin="normal" autoComplete="off" error={Boolean(errors.idToken)} @@ -130,7 +133,7 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { className={classes.button} disabled={!formState?.isDirty || !isEmpty(errors)} > - Continue + {t('signIn.customProvider.continue')}
diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts new file mode 100644 index 0000000000..a9fc17e7f6 --- /dev/null +++ b/packages/core-components/src/translation.ts @@ -0,0 +1,130 @@ +/* + * 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const coreComponentsTranslationRef = createTranslationRef({ + id: 'core-components', + messages: { + signIn: { + title: 'Sign In', + loginFailed: 'Login failed', + customProvider: { + title: 'Custom User', + subtitle: 'Enter your own User ID and credentials.', + subtitle2: 'This selection will not be stored.', + userId: 'User ID', + tokenInvalid: 'Token is not a valid OpenID Connect JWT Token', + continue: 'Continue', + idToken: 'ID Token (optional)', + }, + guestProvider: { + title: 'Guest', + subtitle: 'Enter as a Guest User.', + description: + 'You will not have a verified identity, meaning some features might be unavailable.', + enter: 'Enter', + }, + }, + sidebar: { + shipToContent: 'Skip to content', + starredIntroText: + 'Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!', + recentlyViewedIntroText: + 'And your recently viewed plugins will pop up here!', + dismiss: 'Dismiss', + }, + copyTextButton: { + tooltipText: 'Text copied to clipboard', + }, + simpleStepper: { + reset: 'Reset', + finish: 'Finish', + next: 'Next', + skip: 'Skip', + back: 'Back', + }, + errorPage: { + subtitle: 'ERROR {{status}}: {{statusMessage}}', + title: 'Looks like someone dropped the mic!', + goBack: 'Go back', + orPlease: '... or please ', + contactSupport: 'contact support', + isBug: 'if you think this is a bug.', + }, + emptyState: { + missingAnnotation: { + title: 'Missing Annotation', + actionTitle: + 'Add the annotation to your component YAML as shown in the highlighted example below:', + readMore: 'Read more', + descriptionPrefix_one: 'The annotation ', + descriptionPrefix_other: 'The annotations ', + descriptionSuffix_one: + ' is missing. You need to add the annotation to your component if you want to enable this tool.', + descriptionSuffix_other: + ' are missing. You need to add the annotations to your component if you want to enable this tool.', + }, + }, + supportConfig: { + title: 'Support Not Configured', + links: { + title: 'Add `app.support` config key', + }, + }, + errorBoundary: { + title: 'Please contact {{slackChannel}} for help.', + }, + oauthRequestDialog: { + title: 'Login Required', + authRedirectTitle: 'This will trigger a http redirect to OAuth Login.', + login: 'Log in', + rejectAll: 'Reject All', + }, + link: { + openNewWindow: 'Opens in a new window', + }, + supportButton: { + title: 'Support', + close: 'Close', + }, + table: { + filter: { + title: 'Filters', + clearAll: 'Clear all', + }, + }, + alertDisplay: { + message_one: '({{ num }} older message)', + message_other: '({{ num }} older messages)', + }, + autoLogout: { + stillTherePrompt: { + title: 'Logging out due to inactivity', + description: 'You are about to be disconnected in', + second_one: 'second', + second_other: 'seconds', + descriptionSuffix: 'Are you still there?', + buttonText: "Yes! Don't log me out", + }, + }, + proxiedSignInPage: { + title: + 'You do not appear to be signed in. Please try reloading the browser page.', + }, + }, +}); diff --git a/plugins/badges/src/components/EntityBadgesDialog.test.tsx b/plugins/badges/src/components/EntityBadgesDialog.test.tsx index 5d0e7fe849..94efa1eec8 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.test.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BadgesApi, badgesApiRef } from '../api'; import { EntityBadgesDialog } from './EntityBadgesDialog'; import { EntityProvider } from '@backstage/plugin-catalog-react'; @@ -43,7 +43,7 @@ describe('EntityBadgesDialog', () => { kind: 'MockKind', } as Entity; - const rendered = await renderWithEffects( + const rendered = await renderInTestApp( ({ describe('BitriseArtifactsComponent', () => { entityValue = { entity: { metadata: {} } }; - const renderComponent = () => render(); + const renderComponent = () => renderInTestApp(); it('should display an empty state if an app annotation is missing', async () => { - const rendered = renderComponent(); + const rendered = await renderComponent(); expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument(); }); diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 85b31d6311..5dc5315209 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -52,6 +52,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^9.0.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", diff --git a/plugins/git-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx index abde878648..2bd411b196 100644 --- a/plugins/git-release-manager/src/features/Features.test.tsx +++ b/plugins/git-release-manager/src/features/Features.test.tsx @@ -21,6 +21,10 @@ import { Features } from './Features'; import { mockCalverProject } from '../test-helpers/test-helpers'; import { TEST_IDS } from '../test-helpers/test-ids'; import { mockApiClient } from '../test-helpers/mock-api-client'; +import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -35,18 +39,26 @@ jest.mock('../contexts/ProjectContext', () => ({ describe('Features', () => { it('should omit features omitted via configuration', async () => { const { getByTestId } = render( - [
Custom 1
,
Custom 2
], - }, - }} - />, + + [
Custom 1
,
Custom 2
], + }, + }} + /> + , +
, ); await waitFor(() => getByTestId(TEST_IDS.info.info)); diff --git a/plugins/git-release-manager/src/features/Info/Info.test.tsx b/plugins/git-release-manager/src/features/Info/Info.test.tsx index 1fcd53eb9c..c747a84d6e 100644 --- a/plugins/git-release-manager/src/features/Info/Info.test.tsx +++ b/plugins/git-release-manager/src/features/Info/Info.test.tsx @@ -15,14 +15,13 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; - import { mockCalverProject, mockReleaseBranch, mockReleaseCandidateCalver, } from '../../test-helpers/test-helpers'; import { Info } from './Info'; +import { renderInTestApp } from '@backstage/test-utils'; jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: () => ({ @@ -32,7 +31,7 @@ jest.mock('../../contexts/ProjectContext', () => ({ describe('Info', () => { it('should return early if no latestRelease exists', async () => { - const { findByText } = render( + const { findByText } = await renderInTestApp( ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -56,13 +60,21 @@ describe('PatchBody', () => { }); const { getByTestId } = render( - , + + + , + , ); expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument(); @@ -74,13 +86,20 @@ describe('PatchBody', () => { it('should render not-prerelease description', async () => { const { getByTestId } = render( - , + + + , ); expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument(); diff --git a/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx index ebc3502901..57a219eb50 100644 --- a/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx +++ b/plugins/gocd/src/components/GoCdBuildsComponent/GoCdBuildsComponent.test.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { GoCdBuildsComponent } from './GoCdBuildsComponent'; import { gocdApiRef } from '../../plugin'; import { GoCdApi } from '../../api/gocdApi'; @@ -41,7 +41,7 @@ describe('GoCdArtifactsComponent', () => { }; const renderComponent = () => - renderWithEffects( + renderInTestApp( ', () => { it('renders with mandatory parameters', async () => { - const { getByText } = await render( + const { getByText } = await renderInTestApp( , ); expect(getByText('My title')).toBeInTheDocument(); }); it('renders skeleton when loading is true', async () => { - const { container } = await render( + const { container } = await renderInTestApp( , ); expect(container.querySelectorAll('li')).toHaveLength(8); @@ -36,7 +35,7 @@ describe('', () => { }); it('renders specified amount of items', async () => { - const { container } = await render( + const { container } = await renderInTestApp( ', () => { }); it('renders some items hidden', async () => { - const { container } = await render( + const { container } = await renderInTestApp( ', () => { }); it('renders all items when not collapsed', async () => { - const { container } = await render( + const { container } = await renderInTestApp( ', () => { }); it('renders visit with time-ago', async () => { - const { container, getByText } = await render( - - - , - , + const { container, getByText } = await renderInTestApp( + , ); expect(container.querySelectorAll('li')).toHaveLength(1); expect(getByText('Explore Backstage')).toBeInTheDocument(); @@ -102,23 +98,20 @@ describe('', () => { }); it('renders visit with hits', async () => { - const { container, getByText } = await render( - - - , - , + const { container, getByText } = await renderInTestApp( + , ); expect(container.querySelectorAll('li')).toHaveLength(1); expect(getByText('Explore Backstage')).toBeInTheDocument(); @@ -126,23 +119,20 @@ describe('', () => { }); it('renders text warning about few items', async () => { - const { getByText } = await render( - - - , - , + const { getByText } = await renderInTestApp( + , ); expect( getByText('The more pages you visit, the more pages will appear here.'), @@ -150,10 +140,8 @@ describe('', () => { }); it('renders text warning about no items', async () => { - const { getByText } = await render( - - , - , + const { getByText } = await renderInTestApp( + , ); expect(getByText('There are no visits to show yet.')).toBeInTheDocument(); }); diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx index 68bfce9c96..7972f4401c 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; - import { FixDialog } from './FixDialog'; import { Pod } from 'kubernetes-models/v1/Pod'; +import { renderInTestApp } from '@backstage/test-utils'; jest.mock('../Events', () => ({ Events: () => { @@ -33,8 +32,8 @@ jest.mock('../PodLogs', () => ({ })); describe('FixDialog', () => { - it('docs link should render', () => { - const { getByText } = render( + it('docs link should render', async () => { + const { getByText } = await renderInTestApp( { expect(getByText('fix1')).toBeInTheDocument(); expect(getByText('fix2')).toBeInTheDocument(); }); - it('events button should render', () => { - const { getByText } = render( + it('events button should render', async () => { + const { getByText } = await renderInTestApp( { expect(getByText('fix1')).toBeInTheDocument(); expect(getByText('fix2')).toBeInTheDocument(); }); - it('Logs button should render', () => { - const { getByText } = render( + it('Logs button should render', async () => { + const { getByText } = await renderInTestApp( { @@ -46,7 +46,7 @@ const validResultWithTitle = { describe('TechDocsSearchResultListItem test', () => { it('should render search doc passed in', async () => { - const { findByText } = render( + const { findByText } = await renderInTestApp( , ); @@ -61,7 +61,7 @@ describe('TechDocsSearchResultListItem test', () => { }); it('should use title if defined', async () => { - const { findByText } = render( + const { findByText } = await renderInTestApp( { }); it('should use entity title if defined', async () => { - const { findByText } = render( + const { findByText } = await renderInTestApp( , ); diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx index 15d4334610..872538c5b7 100644 --- a/plugins/todo/src/components/TodoList/TodoList.test.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { TodoApi, todoApiRef } from '../../api'; import { TodoList } from './TodoList'; @@ -43,7 +43,7 @@ describe('TodoList', () => { kind: 'MockKind', } as Entity; - const rendered = await renderWithEffects( + const rendered = await renderInTestApp( diff --git a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx index ec62ea8757..ccf99bb259 100644 --- a/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx +++ b/plugins/vault/src/components/EntityVaultCard/EntityVaultCard.test.tsx @@ -16,9 +16,12 @@ import React from 'react'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { + renderInTestApp, + setupRequestMockHandlers, +} from '@backstage/test-utils'; import { ComponentEntity } from '@backstage/catalog-model'; -import { render, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { EntityVaultCard } from './EntityVaultCard'; import { EntityProvider } from '@backstage/plugin-catalog-react'; @@ -40,7 +43,7 @@ describe('EntityVaultCard', () => { }; it('should render missing entity annotation', async () => { - const rendered = render( + const rendered = await renderInTestApp( , diff --git a/yarn.lock b/yarn.lock index 578ae66433..6ffc2bcb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6722,6 +6722,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/test-utils": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 From f3e5540a68d546f03e7d71c2fb636312b3c42711 Mon Sep 17 00:00:00 2001 From: rui ma Date: Tue, 9 Jan 2024 10:03:40 +0800 Subject: [PATCH 415/483] fix: test faild Signed-off-by: rui ma --- plugins/git-release-manager/src/features/Features.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx index 2bd411b196..ca1517b5dd 100644 --- a/plugins/git-release-manager/src/features/Features.test.tsx +++ b/plugins/git-release-manager/src/features/Features.test.tsx @@ -23,7 +23,7 @@ import { TEST_IDS } from '../test-helpers/test-ids'; import { mockApiClient } from '../test-helpers/mock-api-client'; import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; import { translationApiRef } from '@backstage/core-plugin-api/alpha'; -import { MockTranslationApi } from '@backstage/test-utils'; +import { MockTranslationApi } from '@backstage/test-utils/alpha'; import { errorApiRef } from '@backstage/core-plugin-api'; jest.mock('@backstage/core-plugin-api', () => ({ From 0afea5c0eda27febad0a9f835a2d42ce09be8cfe Mon Sep 17 00:00:00 2001 From: rui ma Date: Thu, 1 Feb 2024 13:45:49 +0800 Subject: [PATCH 416/483] fix: optimize translation keys Signed-off-by: rui ma --- packages/core-components/api-report-alpha.md | 22 +++++-------- .../components/AlertDisplay/AlertDisplay.tsx | 1 - .../src/hooks/useSupportConfig.ts | 4 +-- .../src/layout/ErrorPage/ErrorPage.tsx | 18 ++++++----- .../src/layout/Sidebar/Bar.tsx | 2 +- .../src/layout/SignInPage/customProvider.tsx | 7 +++-- packages/core-components/src/translation.ts | 31 ++++++------------- 7 files changed, 35 insertions(+), 50 deletions(-) diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md index a51e590812..f4f35ae68a 100644 --- a/packages/core-components/api-report-alpha.md +++ b/packages/core-components/api-report-alpha.md @@ -15,20 +15,15 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'signIn.title': 'Sign In'; readonly 'signIn.loginFailed': 'Login failed'; readonly 'signIn.customProvider.title': 'Custom User'; - readonly 'signIn.customProvider.subtitle2': 'This selection will not be stored.'; - readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.'; + readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.'; readonly 'signIn.customProvider.userId': 'User ID'; readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; readonly 'signIn.customProvider.continue': 'Continue'; readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; readonly 'signIn.guestProvider.title': 'Guest'; - readonly 'signIn.guestProvider.description': 'You will not have a verified identity, meaning some features might be unavailable.'; readonly 'signIn.guestProvider.enter': 'Enter'; - readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.'; - readonly 'sidebar.shipToContent': 'Skip to content'; - readonly 'sidebar.starredIntroText': 'Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!'; - readonly 'sidebar.recentlyViewedIntroText': 'And your recently viewed plugins will pop up here!'; - readonly 'sidebar.dismiss': 'Dismiss'; + readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.'; + readonly shipToContent: 'Skip to content'; readonly 'copyTextButton.tooltipText': 'Text copied to clipboard'; readonly 'simpleStepper.finish': 'Finish'; readonly 'simpleStepper.reset': 'Reset'; @@ -38,9 +33,6 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'errorPage.title': 'Looks like someone dropped the mic!'; readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}'; readonly 'errorPage.goBack': 'Go back'; - readonly 'errorPage.orPlease': '... or please '; - readonly 'errorPage.contactSupport': 'contact support'; - readonly 'errorPage.isBug': 'if you think this is a bug.'; readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; readonly 'emptyState.missingAnnotation.readMore': 'Read more'; @@ -48,8 +40,8 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'emptyState.missingAnnotation.descriptionPrefix_other': 'The annotations '; readonly 'emptyState.missingAnnotation.descriptionSuffix_one': ' is missing. You need to add the annotation to your component if you want to enable this tool.'; readonly 'emptyState.missingAnnotation.descriptionSuffix_other': ' are missing. You need to add the annotations to your component if you want to enable this tool.'; - readonly 'supportConfig.title': 'Support Not Configured'; - readonly 'supportConfig.links.title': 'Add `app.support` config key'; + readonly 'supportConfig.default.title': 'Support Not Configured'; + readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key'; readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; readonly 'oauthRequestDialog.title': 'Login Required'; readonly 'oauthRequestDialog.authRedirectTitle': 'This will trigger a http redirect to OAuth Login.'; @@ -57,8 +49,8 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'oauthRequestDialog.rejectAll': 'Reject All'; readonly 'supportButton.title': 'Support'; readonly 'supportButton.close': 'Close'; - readonly 'alertDisplay.message_one': '({{ num }} older message)'; - readonly 'alertDisplay.message_other': '({{ num }} older messages)'; + readonly 'alertDisplay.message_one': '({{ count }} older message)'; + readonly 'alertDisplay.message_other': '({{ count }} older messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; readonly 'autoLogout.stillTherePrompt.description': 'You are about to be disconnected in'; readonly 'autoLogout.stillTherePrompt.second_one': 'second'; diff --git a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx index 19527f76c2..0291b6861d 100644 --- a/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core-components/src/components/AlertDisplay/AlertDisplay.tsx @@ -125,7 +125,6 @@ export function AlertDisplay(props: AlertDisplayProps) { {messages.length > 1 && ( {t('alertDisplay.message', { - num: String(messages.length - 1), count: messages.length - 1, })} diff --git a/packages/core-components/src/hooks/useSupportConfig.ts b/packages/core-components/src/hooks/useSupportConfig.ts index 49cc44c0f7..2b969d8b4d 100644 --- a/packages/core-components/src/hooks/useSupportConfig.ts +++ b/packages/core-components/src/hooks/useSupportConfig.ts @@ -40,12 +40,12 @@ const useDefaultSupportConfig = () => { url: 'https://github.com/backstage/backstage/issues', items: [ { - title: t('supportConfig.title'), + title: t('supportConfig.default.title'), icon: 'warning', links: [ { // TODO: Update to dedicated support page on backstage.io/docs - title: t('supportConfig.links.title'), + title: t('supportConfig.default.linkTitle'), url: 'https://github.com/backstage/backstage/blob/master/app-config.yaml', }, ], diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index f80161ff3c..72c00ed6a9 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -66,7 +66,13 @@ const useStyles = makeStyles( * */ export function ErrorPage(props: IErrorPageProps) { - const { status, statusMessage, additionalInfo, supportUrl, stack } = props; + const { + status = '', + statusMessage, + additionalInfo, + supportUrl, + stack, + } = props; const classes = useStyles(); const navigate = useNavigate(); const support = useSupportConfig(); @@ -80,7 +86,7 @@ export function ErrorPage(props: IErrorPageProps) { variant="body1" className={classes.subtitle} > - {t('errorPage.subtitle', { status: status || '', statusMessage })} + {t('errorPage.subtitle', { status, statusMessage })} {additionalInfo} @@ -92,11 +98,9 @@ export function ErrorPage(props: IErrorPageProps) { navigate(-1)}> {t('errorPage.goBack')} - {t('errorPage.orPlease')} - - {t('errorPage.contactSupport')} - - {t('errorPage.isBug')} + ... or please{' '} + contact support if you + think this is a bug. {stack && }
diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index f8c6036158..200eb0b8e5 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -263,7 +263,7 @@ function A11ySkipSidebar() { variant="contained" className={classnames(classes.visuallyHidden)} > - {t('sidebar.shipToContent')} + {t('shipToContent')} ); } diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index ebab5dce5c..7d1de40d7b 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -46,6 +46,9 @@ const useFormStyles = makeStyles( alignSelf: 'center', marginTop: theme.spacing(2), }, + subTitle: { + whiteSpace: 'pre-line', + }, }), { name: 'BackstageCustomProvider' }, ); @@ -88,10 +91,8 @@ const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => { return ( - + {t('signIn.customProvider.subtitle')} -
- {t('signIn.customProvider.subtitle2')}
diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index a9fc17e7f6..5d7754e765 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -25,8 +25,8 @@ export const coreComponentsTranslationRef = createTranslationRef({ loginFailed: 'Login failed', customProvider: { title: 'Custom User', - subtitle: 'Enter your own User ID and credentials.', - subtitle2: 'This selection will not be stored.', + subtitle: + 'Enter your own User ID and credentials.\n This selection will not be stored.', userId: 'User ID', tokenInvalid: 'Token is not a valid OpenID Connect JWT Token', continue: 'Continue', @@ -34,20 +34,12 @@ export const coreComponentsTranslationRef = createTranslationRef({ }, guestProvider: { title: 'Guest', - subtitle: 'Enter as a Guest User.', - description: - 'You will not have a verified identity, meaning some features might be unavailable.', + subtitle: + 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.', enter: 'Enter', }, }, - sidebar: { - shipToContent: 'Skip to content', - starredIntroText: - 'Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!', - recentlyViewedIntroText: - 'And your recently viewed plugins will pop up here!', - dismiss: 'Dismiss', - }, + shipToContent: 'Skip to content', copyTextButton: { tooltipText: 'Text copied to clipboard', }, @@ -62,9 +54,6 @@ export const coreComponentsTranslationRef = createTranslationRef({ subtitle: 'ERROR {{status}}: {{statusMessage}}', title: 'Looks like someone dropped the mic!', goBack: 'Go back', - orPlease: '... or please ', - contactSupport: 'contact support', - isBug: 'if you think this is a bug.', }, emptyState: { missingAnnotation: { @@ -81,9 +70,9 @@ export const coreComponentsTranslationRef = createTranslationRef({ }, }, supportConfig: { - title: 'Support Not Configured', - links: { - title: 'Add `app.support` config key', + default: { + title: 'Support Not Configured', + linkTitle: 'Add `app.support` config key', }, }, errorBoundary: { @@ -109,8 +98,8 @@ export const coreComponentsTranslationRef = createTranslationRef({ }, }, alertDisplay: { - message_one: '({{ num }} older message)', - message_other: '({{ num }} older messages)', + message_one: '({{ count }} older message)', + message_other: '({{ count }} older messages)', }, autoLogout: { stillTherePrompt: { From 8ab41e507a8860f29af1b8df414c0044506e3bf8 Mon Sep 17 00:00:00 2001 From: rui ma Date: Sun, 18 Feb 2024 11:20:19 +0800 Subject: [PATCH 417/483] fix: StackDetails support translations Signed-off-by: rui ma --- packages/core-components/api-report-alpha.md | 2 ++ .../core-components/src/layout/ErrorPage/StackDetails.tsx | 7 +++++-- packages/core-components/src/translation.ts | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md index f4f35ae68a..210485b38d 100644 --- a/packages/core-components/api-report-alpha.md +++ b/packages/core-components/api-report-alpha.md @@ -33,6 +33,8 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'errorPage.title': 'Looks like someone dropped the mic!'; readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}'; readonly 'errorPage.goBack': 'Go back'; + readonly 'errorPage.showMoreDetails': 'Show more details'; + readonly 'errorPage.showLessDetails': 'Show less details'; readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; readonly 'emptyState.missingAnnotation.readMore': 'Read more'; diff --git a/packages/core-components/src/layout/ErrorPage/StackDetails.tsx b/packages/core-components/src/layout/ErrorPage/StackDetails.tsx index b46a937485..56fabfd5f8 100644 --- a/packages/core-components/src/layout/ErrorPage/StackDetails.tsx +++ b/packages/core-components/src/layout/ErrorPage/StackDetails.tsx @@ -19,6 +19,8 @@ import { useState } from 'react'; import { Link } from '../../components/Link'; import { CodeSnippet } from '../../components'; import { makeStyles } from '@material-ui/core/styles'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { coreComponentsTranslationRef } from '../../translation'; interface IStackDetailsProps { stack: string; @@ -46,6 +48,7 @@ const useStyles = makeStyles( export function StackDetails(props: IStackDetailsProps) { const { stack } = props; const classes = useStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const [detailsOpen, setDetailsOpen] = useState(false); @@ -53,7 +56,7 @@ export function StackDetails(props: IStackDetailsProps) { return ( setDetailsOpen(true)}> - Show more details + {t('errorPage.showMoreDetails')} ); @@ -63,7 +66,7 @@ export function StackDetails(props: IStackDetailsProps) { <> setDetailsOpen(false)}> - Show less details + {t('errorPage.showLessDetails')} Date: Tue, 20 Feb 2024 23:07:37 +0800 Subject: [PATCH 418/483] fix: revert suffix translation keys Signed-off-by: rui ma --- packages/core-components/api-report-alpha.md | 8 -------- .../src/components/AutoLogout/StillTherePrompt.tsx | 8 ++++---- .../EmptyState/MissingAnnotationEmptyState.tsx | 12 +++++------- packages/core-components/src/translation.ts | 10 ---------- 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md index 210485b38d..6cfada994e 100644 --- a/packages/core-components/api-report-alpha.md +++ b/packages/core-components/api-report-alpha.md @@ -38,10 +38,6 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; readonly 'emptyState.missingAnnotation.readMore': 'Read more'; - readonly 'emptyState.missingAnnotation.descriptionPrefix_one': 'The annotation '; - readonly 'emptyState.missingAnnotation.descriptionPrefix_other': 'The annotations '; - readonly 'emptyState.missingAnnotation.descriptionSuffix_one': ' is missing. You need to add the annotation to your component if you want to enable this tool.'; - readonly 'emptyState.missingAnnotation.descriptionSuffix_other': ' are missing. You need to add the annotations to your component if you want to enable this tool.'; readonly 'supportConfig.default.title': 'Support Not Configured'; readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key'; readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; @@ -54,10 +50,6 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'alertDisplay.message_one': '({{ count }} older message)'; readonly 'alertDisplay.message_other': '({{ count }} older messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; - readonly 'autoLogout.stillTherePrompt.description': 'You are about to be disconnected in'; - readonly 'autoLogout.stillTherePrompt.second_one': 'second'; - readonly 'autoLogout.stillTherePrompt.second_other': 'seconds'; - readonly 'autoLogout.stillTherePrompt.descriptionSuffix': 'Are you still there?'; readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; } diff --git a/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx b/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx index c704cf9910..9496199fd9 100644 --- a/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx +++ b/packages/core-components/src/components/AutoLogout/StillTherePrompt.tsx @@ -64,18 +64,18 @@ export const StillTherePrompt = (props: StillTherePromptProps) => { remainingTime - promptTimeoutMillis / 1000, 0, ); + const seconds = timeTillPrompt > 1 ? 'seconds' : 'second'; return ( {t('autoLogout.stillTherePrompt.title')} - {t('autoLogout.stillTherePrompt.description')}{' '} + You are about to be disconnected in{' '} - {Math.ceil(remainingTime / 1000)}{' '} - {t('autoLogout.stillTherePrompt.second', { count: timeTillPrompt })} + {Math.ceil(remainingTime / 1000)} {seconds} - . {t('autoLogout.stillTherePrompt.descriptionSuffix')} + . Are you still there? diff --git a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx index 8c3f8c186f..e185dff8a6 100644 --- a/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core-components/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -79,12 +79,10 @@ function generateComponentYaml(annotations: string[]) { } function useGenerateDescription(annotations: string[]) { - const { t } = useTranslationRef(coreComponentsTranslationRef); + const isSingular = annotations.length <= 1; return ( <> - {t('emptyState.missingAnnotation.descriptionPrefix', { - count: annotations.length, - })} + The {isSingular ? 'annotation' : 'annotations'}{' '} {annotations .map(ann => {ann}) .reduce((prev, curr) => ( @@ -92,9 +90,9 @@ function useGenerateDescription(annotations: string[]) { {prev}, {curr} ))}{' '} - {t('emptyState.missingAnnotation.descriptionSuffix', { - count: annotations.length, - })} + {isSingular ? 'is' : 'are'} missing. You need to add the{' '} + {isSingular ? 'annotation' : 'annotations'} to your component if you want + to enable this tool. ); } diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index bb120f7c18..cc7fe1f3e1 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -63,12 +63,6 @@ export const coreComponentsTranslationRef = createTranslationRef({ actionTitle: 'Add the annotation to your component YAML as shown in the highlighted example below:', readMore: 'Read more', - descriptionPrefix_one: 'The annotation ', - descriptionPrefix_other: 'The annotations ', - descriptionSuffix_one: - ' is missing. You need to add the annotation to your component if you want to enable this tool.', - descriptionSuffix_other: - ' are missing. You need to add the annotations to your component if you want to enable this tool.', }, }, supportConfig: { @@ -106,10 +100,6 @@ export const coreComponentsTranslationRef = createTranslationRef({ autoLogout: { stillTherePrompt: { title: 'Logging out due to inactivity', - description: 'You are about to be disconnected in', - second_one: 'second', - second_other: 'seconds', - descriptionSuffix: 'Are you still there?', buttonText: "Yes! Don't log me out", }, }, From 0f1b3a0e60c06d731e14e294a5e6a4e9030929d2 Mon Sep 17 00:00:00 2001 From: rui ma Date: Tue, 27 Feb 2024 21:12:31 +0800 Subject: [PATCH 419/483] fix: change shipToContent to skipToContent Signed-off-by: rui ma --- packages/core-components/api-report-alpha.md | 2 +- packages/core-components/src/layout/Sidebar/Bar.tsx | 2 +- .../src/layout/SignInPage/guestProvider.tsx | 9 +++++++-- packages/core-components/src/translation.ts | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md index 6cfada994e..f2b21f6d1e 100644 --- a/packages/core-components/api-report-alpha.md +++ b/packages/core-components/api-report-alpha.md @@ -23,7 +23,7 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'signIn.guestProvider.title': 'Guest'; readonly 'signIn.guestProvider.enter': 'Enter'; readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.'; - readonly shipToContent: 'Skip to content'; + readonly skipToContent: 'Skip to content'; readonly 'copyTextButton.tooltipText': 'Text copied to clipboard'; readonly 'simpleStepper.finish': 'Finish'; readonly 'simpleStepper.reset': 'Reset'; diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 200eb0b8e5..c5aa9585bb 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -263,7 +263,7 @@ function A11ySkipSidebar() { variant="contained" className={classnames(classes.visuallyHidden)} > - {t('shipToContent')} + {t('skipToContent')} ); } diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 563d1cc124..e4e02d74fe 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -25,6 +25,8 @@ import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; import { GuestUserIdentity } from './GuestUserIdentity'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { ResponseError } from '@backstage/errors'; +import { coreComponentsTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const getIdentity = async (identity: ProxiedSignInIdentity) => { try { @@ -48,6 +50,7 @@ const Component: ProviderComponent = ({ }) => { const discoveryApi = useApi(discoveryApiRef); const [_, setUseLegacyGuestToken] = useLocalStorage('enableLegacyGuestToken'); + const { t } = useTranslationRef(coreComponentsTranslationRef); const handle = async () => { onSignInStarted(); @@ -85,11 +88,13 @@ const Component: ProviderComponent = ({ variant="fullHeight" actions={ } > - Sign in as a Guest. + + {t('signIn.guestProvider.subtitle')} + ); diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index cc7fe1f3e1..2acb4ea735 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -39,7 +39,7 @@ export const coreComponentsTranslationRef = createTranslationRef({ enter: 'Enter', }, }, - shipToContent: 'Skip to content', + skipToContent: 'Skip to content', copyTextButton: { tooltipText: 'Text copied to clipboard', }, From 07abfe16cb7bf96922d7483fc71bee7540380c8a Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Mon, 26 Feb 2024 11:48:46 +0100 Subject: [PATCH 420/483] feat(notifications): use pagination on the backend layer The NotificationsPage uses pagination by the backend to avoid large datasets to be loaded into frontend. Signed-off-by: Marek Libra --- .changeset/silent-elephants-reflect.md | 6 ++ .../DatabaseNotificationsStore.test.ts | 81 +++++++++++++++++++ .../database/DatabaseNotificationsStore.ts | 11 +++ .../src/database/NotificationsStore.ts | 1 + .../src/service/router.ts | 6 +- .../notifications/src/api/NotificationsApi.ts | 10 ++- .../src/api/NotificationsClient.ts | 5 +- .../NotificationsPage/NotificationsPage.tsx | 18 ++++- .../NotificationsTable/NotificationsTable.tsx | 37 ++++++--- 9 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 .changeset/silent-elephants-reflect.md diff --git a/.changeset/silent-elephants-reflect.md b/.changeset/silent-elephants-reflect.md new file mode 100644 index 0000000000..f9f35421bc --- /dev/null +++ b/.changeset/silent-elephants-reflect.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications': minor +--- + +The NotificationsPage newly uses pagination implemented on the backend layer to avoid large dataset transfers diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index e0af93a1d6..8abb0f1c06 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -215,6 +215,87 @@ describe.each(databases.eachSupportedId())( expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id2); }); + + it('should apply pagination', async () => { + const now = Date.now(); + const id1 = uuid(); + const id2 = uuid(); + const id3 = uuid(); + const id4 = uuid(); + const id5 = uuid(); + const id6 = uuid(); + const id7 = uuid(); + await insertNotification({ + id: id1, + ...testNotification, + created: new Date(Date.now() - 1 * 60 * 60 * 1000 /* an hour ago */), + }); + await insertNotification({ + id: id2, + ...testNotification, + created: new Date(now), + }); + await insertNotification({ + id: id3, + ...testNotification, + created: new Date(now + 1), + }); + await insertNotification({ + id: id4, + ...testNotification, + created: new Date(now + 2), + }); + await insertNotification({ + id: id5, + ...testNotification, + created: new Date(now + 3), + }); + await insertNotification({ + id: id6, + ...testNotification, + created: new Date(now + 4), + }); + await insertNotification({ + id: id7, + ...testNotification, + created: new Date(now + 5), + }); + + await insertNotification({ id: uuid(), ...otherUserNotification }); + + const allUserNotifications = await storage.getNotifications({ + user, + }); + expect(allUserNotifications.length).toBe(7); + + const notifications = await storage.getNotifications({ + user, + createdAfter: new Date(Date.now() - 5 * 60 * 1000 /* 5mins */), + }); + expect(notifications.length).toBe(6); + expect(notifications.at(0)?.id).toEqual(id7); + expect(notifications.at(1)?.id).toEqual(id6); + + const allUserNotificationsPageOne = await storage.getNotifications({ + user, + limit: 3, + offset: 0, + }); + expect(allUserNotificationsPageOne.length).toBe(3); + expect(allUserNotificationsPageOne.at(0)?.id).toEqual(id7); + expect(allUserNotificationsPageOne.at(1)?.id).toEqual(id6); + expect(allUserNotificationsPageOne.at(2)?.id).toEqual(id5); + + const allUserNotificationsPageTwo = await storage.getNotifications({ + user, + limit: 3, + offset: 3, + }); + expect(allUserNotificationsPageTwo.length).toBe(3); + expect(allUserNotificationsPageTwo.at(0)?.id).toEqual(id4); + expect(allUserNotificationsPageTwo.at(1)?.id).toEqual(id3); + expect(allUserNotificationsPageTwo.at(2)?.id).toEqual(id2); + }); }); describe('getStatus', () => { diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 9b654cfd04..afd94fd69d 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -165,6 +165,17 @@ export class DatabaseNotificationsStore implements NotificationsStore { return this.mapToNotifications(notifications); } + async getNotificationsCount(options: NotificationGetOptions) { + const countOptions: NotificationGetOptions = { ...options }; + countOptions.limit = undefined; + countOptions.offset = undefined; + countOptions.sort = null; + const notificationQuery = this.getNotificationsBaseQuery(countOptions); + const response = await notificationQuery.count('* as CNT'); + const totalCount = Number.parseInt(response[0].CNT.toString(), 10); + return totalCount; + } + async saveNotification(notification: Notification) { await this.db .insert(this.mapNotificationToDbRow(notification)) diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 0a7df92f03..03f47263f1 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -42,6 +42,7 @@ export type NotificationModifyOptions = { /** @internal */ export interface NotificationsStore { getNotifications(options: NotificationGetOptions): Promise; + getNotificationsCount(options: NotificationGetOptions): Promise; saveNotification(notification: Notification): Promise; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 78d44dcd44..45a9c56e59 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -213,7 +213,11 @@ export async function createRouter( } const notifications = await store.getNotifications(opts); - res.send(notifications); + const totalCount = await store.getNotificationsCount(opts); + res.send({ + totalCount, + notifications, + }); }); router.get('/:id', async (req, res) => { diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 4a1c792012..da7d88dee1 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -40,9 +40,17 @@ export type UpdateNotificationsOptions = { saved?: boolean; }; +/** @public */ +export type GetNotificationsResponse = { + notifications: Notification[]; + totalCount: number; +}; + /** @public */ export interface NotificationsApi { - getNotifications(options?: GetNotificationsOptions): Promise; + getNotifications( + options?: GetNotificationsOptions, + ): Promise; getNotification(id: string): Promise; diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 1013497b3d..ba0782fe6f 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -15,6 +15,7 @@ */ import { GetNotificationsOptions, + GetNotificationsResponse, NotificationsApi, UpdateNotificationsOptions, } from './NotificationsApi'; @@ -40,7 +41,7 @@ export class NotificationsClient implements NotificationsApi { async getNotifications( options?: GetNotificationsOptions, - ): Promise { + ): Promise { const queryString = new URLSearchParams(); if (options?.limit !== undefined) { queryString.append('limit', options.limit.toString(10)); @@ -59,7 +60,7 @@ export class NotificationsClient implements NotificationsApi { } const urlSegment = `?${queryString}`; - return await this.request(urlSegment); + return await this.request(urlSegment); } async getNotification(id: string): Promise { diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 1f8b141608..81c991798d 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -35,13 +35,18 @@ export const NotificationsPage = () => { const [refresh, setRefresh] = React.useState(false); const { lastSignal } = useSignal('notifications'); const [unreadOnly, setUnreadOnly] = React.useState(true); + const [pageNumber, setPageNumber] = React.useState(0); + const [pageSize, setPageSize] = React.useState(5); const [containsText, setContainsText] = React.useState(); const [createdAfter, setCreatedAfter] = React.useState('lastWeek'); const { error, value, retry, loading } = useNotificationsApi( - // TODO: add pagination and other filters api => { - const options: GetNotificationsOptions = { search: containsText }; + const options: GetNotificationsOptions = { + search: containsText, + limit: pageSize, + offset: pageNumber * pageSize, + }; if (unreadOnly !== undefined) { options.read = !unreadOnly; } @@ -53,7 +58,7 @@ export const NotificationsPage = () => { return api.getNotifications(options); }, - [containsText, unreadOnly, createdAfter], + [containsText, unreadOnly, createdAfter, pageNumber, pageSize], ); useEffect(() => { @@ -94,9 +99,14 @@ export const NotificationsPage = () => { diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 6bcb6badc7..3452a618fb 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -15,25 +15,34 @@ */ import React, { useMemo } from 'react'; import throttle from 'lodash/throttle'; +// @ts-ignore +import RelativeTime from 'react-relative-time'; import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; import { Notification } from '@backstage/plugin-notifications-common'; + import { notificationsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; import MarkAsUnreadIcon from '@material-ui/icons/Markunread'; import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; - -// @ts-ignore -import RelativeTime from 'react-relative-time'; -import { Link, Table, TableColumn } from '@backstage/core-components'; +import { + Link, + Table, + TableProps, + TableColumn, +} from '@backstage/core-components'; const ThrottleDelayMs = 1000; /** @public */ -export type NotificationsTableProps = { +export type NotificationsTableProps = Pick< + TableProps, + 'onPageChange' | 'onRowsPerPageChange' | 'page' | 'totalCount' +> & { isLoading?: boolean; notifications?: Notification[]; onUpdate: () => void; setContainsText: (search: string) => void; + pageSize: number; }; /** @public */ @@ -42,6 +51,11 @@ export const NotificationsTable = ({ notifications = [], onUpdate, setContainsText, + onPageChange, + onRowsPerPageChange, + page, + pageSize, + totalCount, }: NotificationsTableProps) => { const notificationsApi = useApi(notificationsApiRef); @@ -156,16 +170,15 @@ export const NotificationsTable = ({ isLoading={isLoading} options={{ search: true, - // TODO: add pagination - // paging: true, - // pageSize, + paging: true, + pageSize, header: false, sorting: false, }} - // onPageChange={setPageNumber} - // onRowsPerPageChange={setPageSize} - // page={offset} - // totalCount={value?.totalCount} + onPageChange={onPageChange} + onRowsPerPageChange={onRowsPerPageChange} + page={page} + totalCount={totalCount} onSearchChange={throttledContainsTextHandler} data={notifications} columns={compactColumns} From 6f13cda3240de9a9638e7a55a16072ce13029675 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Tue, 27 Feb 2024 14:39:58 +0100 Subject: [PATCH 421/483] chore: clean up after adding createdAfter filter Signed-off-by: Marek Libra --- .../src/database/DatabaseNotificationsStore.ts | 1 - plugins/notifications-backend/src/service/router.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index afd94fd69d..0ff1d10c56 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -99,7 +99,6 @@ export class DatabaseNotificationsStore implements NotificationsStore { ) => { const { user } = options; const isSQLite = this.db.client.config.client.includes('sqlite3'); - // const isPsql = this.db.client.config.client.includes('pg'); const query = this.db('notification').where('user', user); diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 45a9c56e59..b99350e9e4 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -205,7 +205,7 @@ export async function createRouter( // or keep undefined } if (req.query.created_after) { - const sinceEpoch = Date.parse(req.query.created_after.toString()); + const sinceEpoch = Date.parse(String(req.query.created_after)); if (isNaN(sinceEpoch)) { throw new InputError('Unexpected date format'); } From 448650ccac5238bd3484980dfe11abeb8b22decf Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 27 Feb 2024 15:50:13 +0100 Subject: [PATCH 422/483] chore: fix Signed-off-by: blam --- .changeset/sixty-queens-mix.md | 2 -- plugins/scaffolder-backend-module-github/package.json | 2 -- yarn.lock | 2 -- 3 files changed, 6 deletions(-) diff --git a/.changeset/sixty-queens-mix.md b/.changeset/sixty-queens-mix.md index 52f2ca5c39..8fe3f74617 100644 --- a/.changeset/sixty-queens-mix.md +++ b/.changeset/sixty-queens-mix.md @@ -1,7 +1,5 @@ --- '@backstage/plugin-scaffolder-backend': minor -'@backstage/plugin-scaffolder-backend-module-github': patch -'@backstage/plugin-scaffolder-common': patch '@backstage/plugin-scaffolder-node': patch --- diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 62b0415049..15e148b49c 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -42,9 +42,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@backstage/types": "workspace:^", "@octokit/webhooks": "^10.0.0", "libsodium-wrappers": "^0.7.11", "octokit": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 754d563751..1e712c97a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8374,10 +8374,8 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - "@backstage/types": "workspace:^" "@octokit/webhooks": ^10.0.0 "@types/libsodium-wrappers": ^0.7.10 fs-extra: ^11.2.0 From e455dd0ae15ab7c0210e473a19a0766c641f0eaf Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 Feb 2024 16:02:13 +0100 Subject: [PATCH 423/483] Update .changeset/polite-parrots-clap.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- .changeset/polite-parrots-clap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/polite-parrots-clap.md b/.changeset/polite-parrots-clap.md index de05fa949b..de485e0856 100644 --- a/.changeset/polite-parrots-clap.md +++ b/.changeset/polite-parrots-clap.md @@ -1,5 +1,5 @@ --- -'@backstage/create-app': minor +'@backstage/create-app': patch --- Update the search backend template to forward env discovery to the router. From 669efc6f7c2ca3e3dedb100f42a31095fc49ade7 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 27 Feb 2024 09:56:32 -0500 Subject: [PATCH 424/483] chore(adr): remove unused deps Signed-off-by: Phil Kuang --- .changeset/selfish-walls-perform.md | 5 +++++ plugins/adr/knip-report.md | 7 ------- plugins/adr/package.json | 4 +--- yarn.lock | 2 -- 4 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 .changeset/selfish-walls-perform.md diff --git a/.changeset/selfish-walls-perform.md b/.changeset/selfish-walls-perform.md new file mode 100644 index 0000000000..3e7565cf04 --- /dev/null +++ b/.changeset/selfish-walls-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +Remove unused package dependencies diff --git a/plugins/adr/knip-report.md b/plugins/adr/knip-report.md index 91abb6b619..ba9c1dddc5 100644 --- a/plugins/adr/knip-report.md +++ b/plugins/adr/knip-report.md @@ -1,12 +1,5 @@ # Knip report -## Unused dependencies (2) - -| Name | Location | Severity | -| :------------- | :----------- | :------- | -| react-markdown | package.json | error | -| remark-gfm | package.json | error | - ## Unused devDependencies (2) | Name | Location | Severity | diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 1e6f6ece0e..ea426bcd5f 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -58,9 +58,7 @@ "@material-ui/icons": "^4.9.1", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "lodash": "^4.17.21", - "react-markdown": "^8.0.0", - "react-use": "^17.2.4", - "remark-gfm": "^3.0.1" + "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 5d0e70675f..41232a09cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4272,9 +4272,7 @@ __metadata: "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 lodash: ^4.17.21 - react-markdown: ^8.0.0 react-use: ^17.2.4 - remark-gfm: ^3.0.1 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 From a4d16bcfcc8f4b2565d95fb47175f6a08046c1ff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 15:47:33 +0100 Subject: [PATCH 425/483] backend-common: forward service tokens from request if no token manager is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .../auth/authServiceFactory.ts | 2 + .../src/auth/createLegacyAuthAdapters.test.ts | 52 +++++++++++++++++++ .../src/auth/createLegacyAuthAdapters.ts | 43 ++++++++------- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 78ff97a139..ac5c1f9399 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -39,10 +39,12 @@ export type InternalBackstageCredentials = export function createCredentialsWithServicePrincipal( sub: string, + token?: string, ): InternalBackstageCredentials { return { $$type: '@backstage/BackstageCredentials', version: 'v1', + token, principal: { type: 'service', subject: sub, diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts index 43a503199b..354b4a57dc 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.test.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockServices } from '@backstage/backend-test-utils'; import { createLegacyAuthAdapters } from './createLegacyAuthAdapters'; +import { Request } from 'express'; describe('createLegacyAuthAdapters', () => { it('should pass through auth if only auth is provided', () => { @@ -86,4 +88,54 @@ describe('createLegacyAuthAdapters', () => { userInfo: expect.any(Object), }); }); + + it('should forward tokens if no token manager is provided', async () => { + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: undefined, + httpAuth: undefined, + discovery: {} as any, + identity: mockServices.identity(), + }); + + const credentials = await httpAuth.credentials({ + headers: { + authorization: 'Bearer my-token', + }, + } as Request); + + await expect( + auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'test', + }), + ).resolves.toEqual({ token: 'my-token' }); + }); + + it('should issue a new token if a token manager is provided', async () => { + const { auth, httpAuth } = createLegacyAuthAdapters({ + auth: undefined, + httpAuth: undefined, + tokenManager: { + ...mockServices.tokenManager(), + async getToken() { + return { token: 'new-token' }; + }, + }, + discovery: {} as any, + identity: mockServices.identity(), + }); + + const credentials = await httpAuth.credentials({ + headers: { + authorization: 'Bearer mock-token', + }, + } as Request); + + await expect( + auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'test', + }), + ).resolves.toEqual({ token: 'new-token' }); + }); }); diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index a46d553c5d..64dcc81bc6 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -27,7 +27,7 @@ import { TokenManagerService, UserInfoService, } from '@backstage/backend-plugin-api'; -import { ServerTokenManager, TokenManager } from '../tokens'; +import { TokenManager } from '../tokens'; import { AuthenticationError, NotAllowedError } from '@backstage/errors'; import type { Request, Response } from 'express'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -48,7 +48,7 @@ import { PluginEndpointDiscovery } from '../discovery'; class AuthCompat implements AuthService { constructor( private readonly identity: IdentityService, - private readonly tokenManager: TokenManagerService, + private readonly tokenManager?: TokenManagerService, ) {} isPrincipal( @@ -79,9 +79,12 @@ class AuthCompat implements AuthService { } async authenticate(token: string): Promise { - const { aud } = decodeJwt(token); + // Defensively check whether it seems token-like first, just to support + // custom TokenManager implementations that don't emit JWTs specifically. + const payload = + token.split('.').length === 3 ? decodeJwt(token) : undefined; - if (aud === 'backstage') { + if (payload?.aud === 'backstage') { // User Backstage token const identity = await this.identity.getIdentity({ request: { @@ -100,9 +103,12 @@ class AuthCompat implements AuthService { ); } - await this.tokenManager.authenticate(token); + await this.tokenManager?.authenticate(token); - return createCredentialsWithServicePrincipal('external:backstage-plugin'); + return createCredentialsWithServicePrincipal( + 'external:backstage-plugin', + token, + ); } async getPluginRequestToken(options: { @@ -114,8 +120,12 @@ class AuthCompat implements AuthService { switch (type) { // TODO: Check whether the principal is ourselves - case 'service': - return this.tokenManager.getToken(); + case 'service': { + if (this.tokenManager) { + return this.tokenManager.getToken(); + } + return { token: internalForward.token ?? '' }; + } case 'user': if (!internalForward.token) { throw new Error('User credentials is unexpectedly missing token'); @@ -187,17 +197,13 @@ class HttpAuthCompat implements HttpAuthService { async #extractCredentialsFromRequest(req: Request) { const token = getTokenFromRequest(req); if (!token) { - return createCredentialsWithNonePrincipal(); + return this.#auth.getNoneCredentials(); } - const credentials = toInternalBackstageCredentials( - await this.#auth.authenticate(token), - ); - - return credentials; + return this.#auth.authenticate(token); } - async #getCredentials(req: /* */ RequestWithCredentials) { + async #getCredentials(req: RequestWithCredentials) { return (req[credentialsSymbol] ??= this.#extractCredentialsFromRequest(req)); } @@ -209,9 +215,7 @@ class HttpAuthCompat implements HttpAuthService { allowLimitedAccess?: boolean; }, ): Promise> { - const credentials = toInternalBackstageCredentials( - await this.#getCredentials(req), - ); + const credentials = await this.#getCredentials(req); const allowed = options?.allow; if (!allowed) { @@ -335,9 +339,8 @@ export function createLegacyAuthAdapters< const identity = options.identity ?? DefaultIdentityClient.create({ discovery }); - const tokenManager = options.tokenManager ?? ServerTokenManager.noop(); - const authImpl = new AuthCompat(identity, tokenManager); + const authImpl = new AuthCompat(identity, options.tokenManager); const httpAuthImpl = new HttpAuthCompat(authImpl); From 3b0c17f031911c5d64d9dba0126f85df44420bff Mon Sep 17 00:00:00 2001 From: rui ma Date: Tue, 27 Feb 2024 23:16:34 +0800 Subject: [PATCH 426/483] feat: add translationApiRef to storybook wrappers Signed-off-by: rui ma --- storybook/.storybook/apis.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/storybook/.storybook/apis.js b/storybook/.storybook/apis.js index 37cc8eb605..905c5f2dc8 100644 --- a/storybook/.storybook/apis.js +++ b/storybook/.storybook/apis.js @@ -24,6 +24,9 @@ import { featureFlagsApiRef, } from '@backstage/core-plugin-api'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from '@backstage/test-utils/alpha'; + const configApi = new ConfigReader({}); const featureFlagsApi = new LocalStorageFeatureFlags(); const alertApi = new AlertApiForwarder(); @@ -55,6 +58,7 @@ const oktaAuthApi = OktaAuth.create({ basePath: '/auth/', oauthRequestApi, }); +const translationApi = MockTranslationApi.create(); export const apis = [ [configApiRef, configApi], @@ -67,4 +71,5 @@ export const apis = [ [githubAuthApiRef, githubAuthApi], [gitlabAuthApiRef, gitlabAuthApi], [oktaAuthApiRef, oktaAuthApi], + [translationApiRef, translationApi], ]; From 1254f466ef7afb34fc708ba91aa43ea7eb0370eb Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 27 Feb 2024 16:21:39 +0100 Subject: [PATCH 427/483] chore: simplify the input types Signed-off-by: blam --- .../scaffolder-node-test-utils/api-report.md | 17 +---------------- .../src/actions/mockActionConext.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-node-test-utils/api-report.md b/plugins/scaffolder-node-test-utils/api-report.md index 7d508c3c38..d6cda70b48 100644 --- a/plugins/scaffolder-node-test-utils/api-report.md +++ b/plugins/scaffolder-node-test-utils/api-report.md @@ -3,30 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ActionContext } from '@backstage/plugin-scaffolder-node'; import { JsonObject } from '@backstage/types'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; -import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; -import * as winston from 'winston'; -import { Writable } from 'stream'; // @public export const createMockActionContext: < TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, >( - options?: - | { - input?: TActionInput | undefined; - logger?: winston.Logger | undefined; - logStream?: Writable | undefined; - secrets?: TaskSecrets | undefined; - templateInfo?: TemplateInfo | undefined; - workspacePath?: string | undefined; - } - | undefined, + options?: Partial> | undefined, ) => ActionContext; // (No @packageDocumentation comment for this package) diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts index 1b9f6200f9..33a40ad02c 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { PassThrough, Writable } from 'stream'; +import { PassThrough } from 'stream'; import { getVoidLogger } from '@backstage/backend-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; -import { ActionContext, TaskSecrets } from '@backstage/plugin-scaffolder-node'; -import * as winston from 'winston'; -import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; /** * A utility method to create a mock action context for scaffolder actions. @@ -31,14 +29,9 @@ import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; export const createMockActionContext = < TActionInput extends JsonObject = JsonObject, TActionOutput extends JsonObject = JsonObject, ->(options?: { - input?: TActionInput; - logger?: winston.Logger; - logStream?: Writable; - secrets?: TaskSecrets; - templateInfo?: TemplateInfo; - workspacePath?: string; -}): ActionContext => { +>( + options?: Partial>, +): ActionContext => { const defaultContext = { logger: getVoidLogger(), logStream: new PassThrough(), From 85f4723b1bcc3fd051657d470d1d785e8942de0b Mon Sep 17 00:00:00 2001 From: RedlineTriad <39059512+RedlineTriad@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:23:01 +0100 Subject: [PATCH 428/483] fix!: avoid binary file corruption in fetch action The `.toString()` caused all non utf-8 data to be corrupted on disk. This caused issues when downloading binary files such as zip archives. Fixes #22899 Signed-off-by: RedlineTriad <39059512+RedlineTriad@users.noreply.github.com> --- .changeset/eighty-suits-admire.md | 5 ++++ .../scaffolder-node/src/actions/fetch.test.ts | 26 +++++++++++++++++-- plugins/scaffolder-node/src/actions/fetch.ts | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .changeset/eighty-suits-admire.md diff --git a/.changeset/eighty-suits-admire.md b/.changeset/eighty-suits-admire.md new file mode 100644 index 0000000000..5e4b065b2a --- /dev/null +++ b/.changeset/eighty-suits-admire.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +**BREAKING** Fixed file corruption for non utf-8 data in fetch contents diff --git a/plugins/scaffolder-node/src/actions/fetch.test.ts b/plugins/scaffolder-node/src/actions/fetch.test.ts index 42f75d02b4..ea7f458fde 100644 --- a/plugins/scaffolder-node/src/actions/fetch.test.ts +++ b/plugins/scaffolder-node/src/actions/fetch.test.ts @@ -210,7 +210,26 @@ describe('fetchContents helper', () => { fetchUrl: 'https://github.com/backstage/foo', }); expect(fs.ensureDir).toHaveBeenCalledWith('.'); - expect(fs.outputFile).toHaveBeenCalledWith('foo', 'test'); + expect(fs.outputFile).toHaveBeenCalledWith( + 'foo', + Buffer.from([116, 101, 115, 116]), + ); + }); + + it('should fetch binary content from url', async () => { + readUrl.mockResolvedValue({ + buffer: () => Buffer.from([0, 1, 2, 3, 255, 254, 253, 252]), + }); + await fetchFile({ + ...options, + outputPath: 'foo', + fetchUrl: 'https://github.com/backstage/foo', + }); + expect(fs.ensureDir).toHaveBeenCalledWith('.'); + expect(fs.outputFile).toHaveBeenCalledWith( + 'foo', + Buffer.from([0, 1, 2, 3, 255, 254, 253, 252]), + ); }); it('should fetch content from url into directory', async () => { @@ -223,7 +242,10 @@ describe('fetchContents helper', () => { fetchUrl: 'https://github.com/backstage/foo', }); expect(fs.ensureDir).toHaveBeenCalledWith('mydir'); - expect(fs.outputFile).toHaveBeenCalledWith('mydir/foo', 'test'); + expect(fs.outputFile).toHaveBeenCalledWith( + 'mydir/foo', + Buffer.from([116, 101, 115, 116]), + ); }); it('should pass through the token provided through to the URL reader', async () => { diff --git a/plugins/scaffolder-node/src/actions/fetch.ts b/plugins/scaffolder-node/src/actions/fetch.ts index 2ddc2b1ae6..6b3e08bc7e 100644 --- a/plugins/scaffolder-node/src/actions/fetch.ts +++ b/plugins/scaffolder-node/src/actions/fetch.ts @@ -95,7 +95,7 @@ export async function fetchFile(options: { const res = await reader.readUrl(readUrl, { token }); await fs.ensureDir(path.dirname(outputPath)); const buffer = await res.buffer(); - await fs.outputFile(outputPath, buffer.toString()); + await fs.outputFile(outputPath, buffer); } } From 90cf6de034ba1da9573b60404a7c13953630ccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 27 Feb 2024 16:39:32 +0100 Subject: [PATCH 429/483] Update .changeset/eighty-suits-admire.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/eighty-suits-admire.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/eighty-suits-admire.md b/.changeset/eighty-suits-admire.md index 5e4b065b2a..5b889c8087 100644 --- a/.changeset/eighty-suits-admire.md +++ b/.changeset/eighty-suits-admire.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-node': minor +'@backstage/plugin-scaffolder-node': patch --- -**BREAKING** Fixed file corruption for non utf-8 data in fetch contents +Fixed file corruption for non UTF-8 data in fetch contents From 62346b79a7f44432be4334618f165bb30e6d9e18 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 27 Feb 2024 16:15:24 +0100 Subject: [PATCH 430/483] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/polite-parrots-clap.md | 5 ---- .changeset/six-grapes-sniff.md | 2 +- .../backend/src/plugins/search.ts.hbs | 1 - .../search-backend-node/api-report-alpha.md | 29 +------------------ plugins/search-backend-node/src/alpha.ts | 10 ++----- plugins/search-backend/api-report.md | 2 +- plugins/search-backend/src/service/router.ts | 17 +++++++++-- 7 files changed, 20 insertions(+), 46 deletions(-) delete mode 100644 .changeset/polite-parrots-clap.md diff --git a/.changeset/polite-parrots-clap.md b/.changeset/polite-parrots-clap.md deleted file mode 100644 index de485e0856..0000000000 --- a/.changeset/polite-parrots-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Update the search backend template to forward env discovery to the router. diff --git a/.changeset/six-grapes-sniff.md b/.changeset/six-grapes-sniff.md index 43e5ef04f6..095e96dd77 100644 --- a/.changeset/six-grapes-sniff.md +++ b/.changeset/six-grapes-sniff.md @@ -2,4 +2,4 @@ '@backstage/plugin-search-backend': patch --- -**BREAKING**: Update the router to use the new `auth` services. The router now requires a discovery service option to get credentials for the permission service. +Update the router to use the new `auth` services, it now accepts an optional discovery service option to get credentials for the permission service. diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs index 4149f67193..467ac60a5a 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs @@ -60,7 +60,6 @@ export default async function createPlugin( engine: indexBuilder.getSearchEngine(), types: indexBuilder.getDocumentTypes(), permissions: env.permissions, - discovery: env.discovery, config: env.config, logger: env.logger, }); diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index a7ac52a218..c8e5cbb65d 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -3,39 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - -import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; -import { IndexableResultSet } from '@backstage/plugin-search-common'; import { RegisterCollatorParameters } from '@backstage/plugin-search-backend-node'; import { RegisterDecoratorParameters } from '@backstage/plugin-search-backend-node'; -import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { ServiceRef } from '@backstage/backend-plugin-api'; -import { Writable } from 'stream'; - -// @public -export type QueryRequestOptions = - | { - token?: string; - } - | { - credentials: BackstageCredentials; - }; - -// @public -export type QueryTranslator = (query: SearchQuery) => unknown; - -// @public -export interface SearchEngine { - getIndexer(type: string): Promise; - query( - query: SearchQuery, - options?: QueryRequestOptions, - ): Promise; - setTranslator(translator: QueryTranslator): void; -} // @alpha export interface SearchEngineRegistryExtensionPoint { diff --git a/plugins/search-backend-node/src/alpha.ts b/plugins/search-backend-node/src/alpha.ts index a491e158b4..272bef7f84 100644 --- a/plugins/search-backend-node/src/alpha.ts +++ b/plugins/search-backend-node/src/alpha.ts @@ -30,14 +30,10 @@ import { RegisterDecoratorParameters, } from '@backstage/plugin-search-backend-node'; -import { SearchEngine } from './types'; -import { IndexBuilder } from './IndexBuilder'; - -export type { +import { SearchEngine, - QueryRequestOptions, - QueryTranslator, -} from './types'; + IndexBuilder, +} from '@backstage/plugin-search-backend-node'; /** * @alpha diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index f868ed43a3..4f99632f28 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -21,7 +21,7 @@ export function createRouter(options: RouterOptions): Promise; export type RouterOptions = { engine: SearchEngine; types: Record; - discovery: DiscoveryService; + discovery?: DiscoveryService; permissions: PermissionEvaluator | PermissionAuthorizer; config: Config; logger: Logger; diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 9f811d5d06..ae470ba40e 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -18,6 +18,7 @@ import express from 'express'; import { Logger } from 'winston'; import { z } from 'zod'; import { + HostDiscovery, createLegacyAuthAdapters, errorHandler, } from '@backstage/backend-common'; @@ -64,7 +65,7 @@ const jsonObjectSchema: z.ZodSchema = z.lazy(() => { export type RouterOptions = { engine: SearchEngine; types: Record; - discovery: DiscoveryService; + discovery?: DiscoveryService; permissions: PermissionEvaluator | PermissionAuthorizer; config: Config; logger: Logger; @@ -83,9 +84,19 @@ export async function createRouter( options: RouterOptions, ): Promise { const router = await createOpenApiRouter(); - const { engine: inputEngine, types, permissions, config, logger } = options; + const { + engine: inputEngine, + types, + permissions, + config, + logger, + discovery = HostDiscovery.fromConfig(config), + } = options; - const { auth, httpAuth } = createLegacyAuthAdapters(options); + const { auth, httpAuth } = createLegacyAuthAdapters({ + ...options, + discovery, + }); const maxPageLimit = config.getOptionalNumber('search.maxPageLimit') ?? defaultMaxPageLimit; From d3d8f905f4fd96ef8e413c83113e107aea8d6867 Mon Sep 17 00:00:00 2001 From: Avantika Iyer Date: Tue, 27 Feb 2024 15:42:55 +0000 Subject: [PATCH 431/483] remove adr from default search filters Signed-off-by: Avantika Iyer --- packages/app/src/components/search/SearchPage.tsx | 5 ----- plugins/search/src/alpha.tsx | 5 ----- 2 files changed, 10 deletions(-) diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index a9f54cdb49..d904aef45c 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -87,11 +87,6 @@ const SearchPage = () => { name: 'Documentation', icon: , }, - { - value: 'adr', - name: 'Architecture Decision Records', - icon: , - }, ]} /> diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 4f654548f5..f4dd09088a 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -153,11 +153,6 @@ export const searchPage = createPageExtension({ name: 'Documentation', icon: , }, - { - value: 'adr', - name: 'Architecture Decision Records', - icon: , - }, ]} /> From f0464b06d9f3c43f5227a67f18df6c84b1c137d7 Mon Sep 17 00:00:00 2001 From: Avantika Iyer Date: Tue, 27 Feb 2024 15:57:54 +0000 Subject: [PATCH 432/483] add changeset Signed-off-by: Avantika Iyer --- .changeset/old-ducks-fetch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/old-ducks-fetch.md diff --git a/.changeset/old-ducks-fetch.md b/.changeset/old-ducks-fetch.md new file mode 100644 index 0000000000..0e719d07e1 --- /dev/null +++ b/.changeset/old-ducks-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Removes ADR from the default set of search filters From e920db20c3fe3b39df90c6175edd553f95ec2658 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 27 Feb 2024 16:12:11 +0000 Subject: [PATCH 433/483] Remove await not needed Signed-off-by: Brian Fletcher --- packages/backend/src/plugins/catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 00fe7ff4a0..9b92b4eb06 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -24,7 +24,7 @@ import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - const builder = await CatalogBuilder.create(env); + const builder = CatalogBuilder.create(env); builder.addProcessor(new ScaffolderEntitiesProcessor()); const demoProvider = new DemoEventBasedEntityProvider({ From c443bed48a93bd7921726dcb2908d1b4dd564e57 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Feb 2024 17:29:06 +0100 Subject: [PATCH 434/483] catalog-react: roll back style class key change Signed-off-by: Patrik Oldsberg --- plugins/catalog-react/api-report.md | 7 ++----- .../src/components/EntityOwnerPicker/EntityOwnerPicker.tsx | 2 +- .../EntityProcessingStatusPicker.tsx | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index a492a72e9f..57fa247171 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -109,13 +109,10 @@ export type CatalogReactEntityLifecyclePickerClassKey = 'input'; export type CatalogReactEntityNamespacePickerClassKey = 'input'; // @public (undocumented) -export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; +export type CatalogReactEntityOwnerPickerClassKey = 'input'; // @public (undocumented) -export type CatalogReactEntityProcessingStatusPickerClassKey = - | 'input' - | 'root' - | 'label'; +export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; // @public (undocumented) export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 87de6411b2..abba751e28 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -44,7 +44,7 @@ import { withStyles } from '@material-ui/core/styles'; import { useEntityPresentation } from '../../apis'; /** @public */ -export type CatalogReactEntityOwnerPickerClassKey = 'input' | 'root' | 'label'; +export type CatalogReactEntityOwnerPickerClassKey = 'input'; const useStyles = makeStyles( { diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index 2f63323967..e62fa7a6c7 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -31,10 +31,7 @@ import { useEntityList } from '../../hooks'; import { Autocomplete } from '@material-ui/lab'; /** @public */ -export type CatalogReactEntityProcessingStatusPickerClassKey = - | 'input' - | 'root' - | 'label'; +export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; const useStyles = makeStyles( { From 4cca80fbf8467351c97383e67b28d0c57d16ff6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 27 Feb 2024 16:48:30 +0000 Subject: [PATCH 435/483] Version Packages (next) --- .changeset/create-app-1709052411.md | 5 + .changeset/pre.json | 111 +- docs/releases/v1.24.0-next.0-changelog.md | 4281 +++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 11 + 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 | 76 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 79 + packages/app/package.json | 2 +- packages/backend-app-api/CHANGELOG.md | 26 + packages/backend-app-api/package.json | 2 +- packages/backend-common/CHANGELOG.md | 30 + packages/backend-common/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 9 + packages/backend-defaults/package.json | 2 +- .../CHANGELOG.md | 27 + .../package.json | 2 +- packages/backend-next/CHANGELOG.md | 48 + packages/backend-next/package.json | 2 +- packages/backend-openapi-utils/CHANGELOG.md | 9 + packages/backend-openapi-utils/package.json | 2 +- packages/backend-plugin-api/CHANGELOG.md | 18 + packages/backend-plugin-api/package.json | 2 +- packages/backend-tasks/CHANGELOG.md | 12 + packages/backend-tasks/package.json | 2 +- packages/backend-test-utils/CHANGELOG.md | 22 + packages/backend-test-utils/package.json | 2 +- packages/backend/CHANGELOG.md | 60 + packages/backend/package.json | 2 +- packages/catalog-client/CHANGELOG.md | 8 + packages/catalog-client/package.json | 2 +- packages/catalog-model/CHANGELOG.md | 8 + packages/catalog-model/package.json | 2 +- packages/cli-node/CHANGELOG.md | 9 + packages/cli-node/package.json | 2 +- packages/cli/CHANGELOG.md | 19 + packages/cli/package.json | 2 +- packages/config-loader/CHANGELOG.md | 10 + packages/config-loader/package.json | 2 +- packages/config/CHANGELOG.md | 8 + packages/config/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 10 + 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 | 16 + packages/core-components/package.json | 2 +- packages/core-plugin-api/CHANGELOG.md | 10 + packages/core-plugin-api/package.json | 2 +- packages/create-app/CHANGELOG.md | 8 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 14 + packages/dev-utils/package.json | 2 +- packages/e2e-test/CHANGELOG.md | 9 + packages/e2e-test/package.json | 2 +- packages/errors/CHANGELOG.md | 8 + packages/errors/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 6 + packages/eslint-plugin/package.json | 2 +- packages/frontend-app-api/CHANGELOG.md | 15 + packages/frontend-app-api/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 10 + packages/frontend-plugin-api/package.json | 2 +- packages/frontend-test-utils/CHANGELOG.md | 10 + packages/frontend-test-utils/package.json | 2 +- packages/integration-aws-node/CHANGELOG.md | 8 + packages/integration-aws-node/package.json | 2 +- packages/integration-react/CHANGELOG.md | 10 + packages/integration-react/package.json | 2 +- packages/integration/CHANGELOG.md | 8 + packages/integration/package.json | 2 +- packages/repo-tools/CHANGELOG.md | 12 + packages/repo-tools/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 19 + .../techdocs-cli-embedded-app/package.json | 2 +- packages/techdocs-cli/CHANGELOG.md | 11 + packages/techdocs-cli/package.json | 2 +- packages/test-utils/CHANGELOG.md | 13 + packages/test-utils/package.json | 2 +- packages/theme/CHANGELOG.md | 6 + packages/theme/package.json | 2 +- plugins/adr-backend/CHANGELOG.md | 17 + plugins/adr-backend/package.json | 2 +- plugins/adr-common/CHANGELOG.md | 10 + plugins/adr-common/package.json | 2 +- plugins/adr/CHANGELOG.md | 17 + plugins/adr/package.json | 2 +- plugins/airbrake-backend/CHANGELOG.md | 9 + plugins/airbrake-backend/package.json | 2 +- plugins/airbrake/CHANGELOG.md | 12 + plugins/airbrake/package.json | 2 +- plugins/allure/CHANGELOG.md | 10 + plugins/allure/package.json | 2 +- plugins/analytics-module-ga/CHANGELOG.md | 10 + plugins/analytics-module-ga/package.json | 2 +- plugins/analytics-module-ga4/CHANGELOG.md | 10 + plugins/analytics-module-ga4/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/apache-airflow/CHANGELOG.md | 8 + plugins/apache-airflow/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 16 + plugins/api-docs/package.json | 2 +- plugins/apollo-explorer/CHANGELOG.md | 8 + plugins/apollo-explorer/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 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 34 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/auth-backend/CHANGELOG.md | 37 + plugins/auth-backend/package.json | 2 +- plugins/auth-node/CHANGELOG.md | 18 + plugins/auth-node/package.json | 2 +- plugins/azure-devops-backend/CHANGELOG.md | 27 + plugins/azure-devops-backend/package.json | 2 +- plugins/azure-devops-common/CHANGELOG.md | 14 + plugins/azure-devops-common/package.json | 2 +- plugins/azure-devops/CHANGELOG.md | 30 + plugins/azure-devops/package.json | 2 +- plugins/azure-sites-backend/CHANGELOG.md | 22 + plugins/azure-sites-backend/package.json | 2 +- plugins/azure-sites-common/CHANGELOG.md | 9 + plugins/azure-sites-common/package.json | 2 +- plugins/azure-sites/CHANGELOG.md | 14 + plugins/azure-sites/package.json | 2 +- plugins/badges-backend/CHANGELOG.md | 14 + plugins/badges-backend/package.json | 2 +- plugins/badges/CHANGELOG.md | 11 + plugins/badges/package.json | 2 +- plugins/bazaar-backend/CHANGELOG.md | 10 + plugins/bazaar-backend/package.json | 2 +- plugins/bazaar/CHANGELOG.md | 12 + plugins/bazaar/package.json | 2 +- plugins/bitbucket-cloud-common/CHANGELOG.md | 7 + plugins/bitbucket-cloud-common/package.json | 2 +- plugins/bitrise/CHANGELOG.md | 10 + plugins/bitrise/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 19 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 15 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 58 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 13 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 21 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 15 + .../catalog-backend-module-ldap/package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 33 + plugins/catalog-backend/package.json | 2 +- plugins/catalog-common/CHANGELOG.md | 9 + plugins/catalog-common/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 14 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 19 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 18 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 19 + plugins/catalog-react/package.json | 2 +- .../catalog-unprocessed-entities/CHANGELOG.md | 10 + .../catalog-unprocessed-entities/package.json | 2 +- plugins/catalog/CHANGELOG.md | 23 + plugins/catalog/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/cicd-statistics/CHANGELOG.md | 9 + plugins/cicd-statistics/package.json | 2 +- plugins/circleci/CHANGELOG.md | 10 + plugins/circleci/package.json | 2 +- plugins/cloudbuild/CHANGELOG.md | 10 + plugins/cloudbuild/package.json | 2 +- plugins/code-climate/CHANGELOG.md | 10 + plugins/code-climate/package.json | 2 +- plugins/code-coverage-backend/CHANGELOG.md | 18 + plugins/code-coverage-backend/package.json | 2 +- plugins/code-coverage/CHANGELOG.md | 11 + plugins/code-coverage/package.json | 2 +- plugins/codescene/CHANGELOG.md | 10 + plugins/codescene/package.json | 2 +- plugins/config-schema/CHANGELOG.md | 10 + plugins/config-schema/package.json | 2 +- plugins/cost-insights/CHANGELOG.md | 14 + plugins/cost-insights/package.json | 2 +- plugins/devtools-backend/CHANGELOG.md | 21 + plugins/devtools-backend/package.json | 2 +- plugins/devtools-common/CHANGELOG.md | 8 + plugins/devtools-common/package.json | 2 +- plugins/devtools/CHANGELOG.md | 14 + plugins/devtools/package.json | 2 +- plugins/dynatrace/CHANGELOG.md | 10 + plugins/dynatrace/package.json | 2 +- plugins/entity-feedback-backend/CHANGELOG.md | 14 + plugins/entity-feedback-backend/package.json | 2 +- plugins/entity-feedback/CHANGELOG.md | 12 + plugins/entity-feedback/package.json | 2 +- plugins/entity-validation/CHANGELOG.md | 13 + plugins/entity-validation/package.json | 2 +- .../CHANGELOG.md | 43 + .../package.json | 2 +- .../events-backend-module-azure/CHANGELOG.md | 78 + .../events-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 78 + .../package.json | 2 +- .../events-backend-module-gerrit/CHANGELOG.md | 78 + .../events-backend-module-gerrit/package.json | 2 +- .../events-backend-module-github/CHANGELOG.md | 79 + .../events-backend-module-github/package.json | 2 +- .../events-backend-module-gitlab/CHANGELOG.md | 79 + .../events-backend-module-gitlab/package.json | 2 +- .../events-backend-test-utils/CHANGELOG.md | 41 + .../events-backend-test-utils/package.json | 2 +- plugins/events-backend/CHANGELOG.md | 78 + plugins/events-backend/package.json | 2 +- plugins/events-node/CHANGELOG.md | 111 + plugins/events-node/package.json | 2 +- .../example-todo-list-backend/CHANGELOG.md | 10 + .../example-todo-list-backend/package.json | 2 +- plugins/example-todo-list-common/CHANGELOG.md | 7 + plugins/example-todo-list-common/package.json | 2 +- plugins/example-todo-list/CHANGELOG.md | 8 + plugins/example-todo-list/package.json | 2 +- plugins/explore-backend/CHANGELOG.md | 12 + plugins/explore-backend/package.json | 2 +- plugins/explore-react/CHANGELOG.md | 8 + plugins/explore-react/package.json | 2 +- plugins/explore/CHANGELOG.md | 16 + plugins/explore/package.json | 2 +- plugins/firehydrant/CHANGELOG.md | 10 + plugins/firehydrant/package.json | 2 +- plugins/fossa/CHANGELOG.md | 11 + plugins/fossa/package.json | 2 +- plugins/gcalendar/CHANGELOG.md | 9 + plugins/gcalendar/package.json | 2 +- plugins/gcp-projects/CHANGELOG.md | 8 + plugins/gcp-projects/package.json | 2 +- plugins/git-release-manager/CHANGELOG.md | 9 + plugins/git-release-manager/package.json | 2 +- plugins/github-actions/CHANGELOG.md | 12 + plugins/github-actions/package.json | 2 +- plugins/github-deployments/CHANGELOG.md | 13 + plugins/github-deployments/package.json | 2 +- plugins/github-issues/CHANGELOG.md | 12 + plugins/github-issues/package.json | 2 +- .../github-pull-requests-board/CHANGELOG.md | 13 + .../github-pull-requests-board/package.json | 2 +- plugins/gitops-profiles/CHANGELOG.md | 8 + plugins/gitops-profiles/package.json | 2 +- plugins/gocd/CHANGELOG.md | 11 + plugins/gocd/package.json | 2 +- plugins/graphiql/CHANGELOG.md | 10 + plugins/graphiql/package.json | 2 +- plugins/graphql-voyager/CHANGELOG.md | 8 + plugins/graphql-voyager/package.json | 2 +- plugins/home-react/CHANGELOG.md | 12 + plugins/home-react/package.json | 2 +- plugins/home/CHANGELOG.md | 21 + plugins/home/package.json | 2 +- plugins/ilert/CHANGELOG.md | 11 + plugins/ilert/package.json | 2 +- plugins/jenkins-backend/CHANGELOG.md | 23 + plugins/jenkins-backend/package.json | 2 +- plugins/jenkins-common/CHANGELOG.md | 8 + plugins/jenkins-common/package.json | 2 +- plugins/jenkins/CHANGELOG.md | 12 + plugins/jenkins/package.json | 2 +- plugins/kafka-backend/CHANGELOG.md | 10 + plugins/kafka-backend/package.json | 2 +- plugins/kafka/CHANGELOG.md | 10 + plugins/kafka/package.json | 2 +- plugins/kubernetes-backend/CHANGELOG.md | 24 + plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 12 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes-common/CHANGELOG.md | 10 + plugins/kubernetes-common/package.json | 2 +- plugins/kubernetes-node/CHANGELOG.md | 10 + plugins/kubernetes-node/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 13 + plugins/kubernetes-react/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 12 + plugins/kubernetes/package.json | 2 +- plugins/lighthouse-backend/CHANGELOG.md | 16 + plugins/lighthouse-backend/package.json | 2 +- plugins/lighthouse-common/CHANGELOG.md | 7 + plugins/lighthouse-common/package.json | 2 +- plugins/lighthouse/CHANGELOG.md | 11 + plugins/lighthouse/package.json | 2 +- plugins/linguist-backend/CHANGELOG.md | 21 + plugins/linguist-backend/package.json | 2 +- plugins/linguist/CHANGELOG.md | 17 + plugins/linguist/package.json | 2 +- plugins/microsoft-calendar/CHANGELOG.md | 9 + plugins/microsoft-calendar/package.json | 2 +- plugins/newrelic-dashboard/CHANGELOG.md | 11 + plugins/newrelic-dashboard/package.json | 2 +- plugins/newrelic/CHANGELOG.md | 8 + plugins/newrelic/package.json | 2 +- plugins/nomad-backend/CHANGELOG.md | 10 + plugins/nomad-backend/package.json | 2 +- plugins/nomad/CHANGELOG.md | 10 + plugins/nomad/package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 25 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-common/CHANGELOG.md | 6 + plugins/notifications-common/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 18 + plugins/notifications-node/package.json | 2 +- plugins/notifications/CHANGELOG.md | 18 + plugins/notifications/package.json | 2 +- plugins/octopus-deploy/CHANGELOG.md | 10 + plugins/octopus-deploy/package.json | 2 +- plugins/opencost/CHANGELOG.md | 8 + plugins/opencost/package.json | 2 +- plugins/org-react/CHANGELOG.md | 11 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 14 + plugins/org/package.json | 2 +- plugins/pagerduty/CHANGELOG.md | 12 + plugins/pagerduty/package.json | 2 +- plugins/periskop-backend/CHANGELOG.md | 9 + plugins/periskop-backend/package.json | 2 +- plugins/periskop/CHANGELOG.md | 11 + plugins/periskop/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/permission-backend/CHANGELOG.md | 17 + plugins/permission-backend/package.json | 2 +- plugins/permission-common/CHANGELOG.md | 12 + plugins/permission-common/package.json | 2 +- plugins/permission-node/CHANGELOG.md | 13 + plugins/permission-node/package.json | 2 +- plugins/permission-react/CHANGELOG.md | 9 + plugins/permission-react/package.json | 2 +- plugins/playlist-backend/CHANGELOG.md | 19 + plugins/playlist-backend/package.json | 2 +- plugins/playlist-common/CHANGELOG.md | 7 + plugins/playlist-common/package.json | 2 +- plugins/playlist/CHANGELOG.md | 16 + plugins/playlist/package.json | 2 +- plugins/proxy-backend/CHANGELOG.md | 12 + plugins/proxy-backend/package.json | 2 +- plugins/puppetdb/CHANGELOG.md | 11 + plugins/puppetdb/package.json | 2 +- plugins/rollbar-backend/CHANGELOG.md | 8 + plugins/rollbar-backend/package.json | 2 +- plugins/rollbar/CHANGELOG.md | 10 + plugins/rollbar/package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 18 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 17 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 17 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 17 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 40 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-common/CHANGELOG.md | 9 + plugins/scaffolder-common/package.json | 2 +- .../scaffolder-node-test-utils/CHANGELOG.md | 17 + .../scaffolder-node-test-utils/package.json | 2 +- plugins/scaffolder-node/CHANGELOG.md | 15 + plugins/scaffolder-node/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 21 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 25 + plugins/scaffolder/package.json | 2 +- .../CHANGELOG.md | 19 + .../package.json | 2 +- .../CHANGELOG.md | 15 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- plugins/search-backend-module-pg/CHANGELOG.md | 14 + plugins/search-backend-module-pg/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 19 + .../package.json | 2 +- plugins/search-backend-node/CHANGELOG.md | 16 + plugins/search-backend-node/package.json | 2 +- plugins/search-backend/CHANGELOG.md | 18 + plugins/search-backend/package.json | 2 +- plugins/search-common/CHANGELOG.md | 9 + plugins/search-common/package.json | 2 +- plugins/search-react/CHANGELOG.md | 13 + plugins/search-react/package.json | 2 +- plugins/search/CHANGELOG.md | 17 + plugins/search/package.json | 2 +- plugins/sentry/CHANGELOG.md | 10 + plugins/sentry/package.json | 2 +- plugins/shortcuts/CHANGELOG.md | 12 + plugins/shortcuts/package.json | 2 +- plugins/signals-backend/CHANGELOG.md | 15 + plugins/signals-backend/package.json | 2 +- plugins/signals-node/CHANGELOG.md | 14 + plugins/signals-node/package.json | 2 +- plugins/signals-react/CHANGELOG.md | 8 + plugins/signals-react/package.json | 2 +- plugins/signals/CHANGELOG.md | 13 + plugins/signals/package.json | 2 +- plugins/sonarqube-backend/CHANGELOG.md | 10 + plugins/sonarqube-backend/package.json | 2 +- plugins/sonarqube-react/CHANGELOG.md | 8 + plugins/sonarqube-react/package.json | 2 +- plugins/sonarqube/CHANGELOG.md | 11 + plugins/sonarqube/package.json | 2 +- plugins/splunk-on-call/CHANGELOG.md | 10 + plugins/splunk-on-call/package.json | 2 +- plugins/stack-overflow-backend/CHANGELOG.md | 7 + plugins/stack-overflow-backend/package.json | 2 +- plugins/stack-overflow/CHANGELOG.md | 14 + plugins/stack-overflow/package.json | 2 +- plugins/stackstorm/CHANGELOG.md | 9 + plugins/stackstorm/package.json | 2 +- .../CHANGELOG.md | 13 + .../package.json | 2 +- plugins/tech-insights-backend/CHANGELOG.md | 19 + plugins/tech-insights-backend/package.json | 2 +- plugins/tech-insights-node/CHANGELOG.md | 15 + plugins/tech-insights-node/package.json | 2 +- plugins/tech-insights/CHANGELOG.md | 13 + plugins/tech-insights/package.json | 2 +- plugins/tech-radar/CHANGELOG.md | 11 + plugins/tech-radar/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 15 + .../techdocs-addons-test-utils/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 17 + plugins/techdocs-backend/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- plugins/techdocs-node/CHANGELOG.md | 15 + plugins/techdocs-node/package.json | 2 +- plugins/techdocs-react/CHANGELOG.md | 11 + plugins/techdocs-react/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 20 + plugins/techdocs/package.json | 2 +- plugins/todo-backend/CHANGELOG.md | 16 + plugins/todo-backend/package.json | 2 +- plugins/todo/CHANGELOG.md | 11 + plugins/todo/package.json | 2 +- plugins/user-settings-backend/CHANGELOG.md | 12 + plugins/user-settings-backend/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 15 + plugins/user-settings/package.json | 2 +- plugins/vault-backend/CHANGELOG.md | 12 + plugins/vault-backend/package.json | 2 +- plugins/vault-node/CHANGELOG.md | 7 + plugins/vault-node/package.json | 2 +- plugins/vault/CHANGELOG.md | 11 + plugins/vault/package.json | 2 +- plugins/xcmetrics/CHANGELOG.md | 9 + plugins/xcmetrics/package.json | 2 +- yarn.lock | 356 +- 529 files changed, 9255 insertions(+), 290 deletions(-) create mode 100644 .changeset/create-app-1709052411.md create mode 100644 docs/releases/v1.24.0-next.0-changelog.md create mode 100644 plugins/auth-backend-module-guest-provider/CHANGELOG.md diff --git a/.changeset/create-app-1709052411.md b/.changeset/create-app-1709052411.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1709052411.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 8a921bf194..6b08809e2a 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -274,7 +274,114 @@ "@backstage/plugin-vault": "0.1.25", "@backstage/plugin-vault-backend": "0.4.3", "@backstage/plugin-vault-node": "0.1.3", - "@backstage/plugin-xcmetrics": "0.2.48" + "@backstage/plugin-xcmetrics": "0.2.48", + "@backstage/plugin-auth-backend-module-guest-provider": "0.0.0", + "@backstage/plugin-scaffolder-node-test-utils": "0.0.1" }, - "changesets": [] + "changesets": [ + "big-yaks-film", + "breezy-cycles-count", + "bright-bulldogs-whisper", + "calm-pans-work", + "chilled-dolls-accept", + "chilled-dolphins-tap", + "chilled-goats-matter", + "clever-eagles-boil", + "cold-boats-sell", + "cold-dolphins-raise", + "create-app-1709052411", + "cyan-dryers-share", + "dirty-apes-divide", + "dry-impalas-serve", + "eight-fireants-crash", + "eighty-suits-admire", + "eleven-cows-learn", + "empty-wolves-rule", + "fast-buses-exercise", + "fifty-insects-yell", + "fifty-moons-study", + "five-beers-accept", + "five-hats-accept", + "five-mayflies-juggle", + "flat-badgers-attack", + "forty-oranges-joke", + "fresh-rings-tell", + "friendly-coats-travel", + "friendly-news-sin", + "funny-flies-collect", + "healthy-experts-rhyme", + "heavy-coats-sniff", + "hungry-points-burn", + "itchy-news-drive", + "kind-pants-speak", + "kind-students-cross", + "late-turkeys-remember", + "lazy-needles-lick", + "lazy-terms-shake", + "lemon-lemons-sparkle", + "long-emus-talk", + "loud-dolls-exist", + "lovely-donkeys-kneel", + "modern-impalas-add", + "neat-owls-pump", + "nervous-lions-suffer", + "nice-beans-wait", + "odd-toys-wonder", + "old-ducks-fetch", + "olive-mails-tell", + "perfect-taxis-give", + "polite-tips-begin", + "polite-zoos-pay", + "poor-beans-cross", + "poor-ladybugs-smell", + "pretty-boats-promise", + "purple-kiwis-complain", + "rare-dryers-check", + "red-taxis-swim", + "renovate-0300bde", + "renovate-08c5b50", + "renovate-1c2c49d", + "renovate-58582bb", + "renovate-5d40e90", + "renovate-6a81dd3", + "renovate-755938a", + "renovate-7aa519f", + "renovate-8f23b96", + "renovate-914f0df", + "renovate-9850908", + "renovate-ea48bac", + "rude-masks-tan", + "rude-sheep-jam", + "selfish-glasses-cheer", + "selfish-walls-perform", + "silver-flowers-trade", + "silver-impalas-run", + "six-grapes-sniff", + "six-nails-hammer", + "six-sloths-listen", + "sixty-queens-mix", + "slimy-trainers-attend", + "slow-readers-clap", + "smart-owls-tease", + "soft-grapes-cough", + "soft-otters-report", + "sour-olives-carry", + "spicy-dragons-sin", + "tasty-beans-confess", + "ten-spoons-help", + "tender-carrots-care", + "thick-pillows-develop", + "thin-spiders-do", + "thirty-shirts-allow", + "tiny-books-destroy", + "tiny-bugs-enjoy", + "tricky-months-hug", + "two-planets-beam", + "two-snails-fry", + "unlucky-jobs-report", + "unlucky-lizards-suffer", + "violet-rocks-rescue", + "wet-sheep-reply", + "young-flies-wash" + ] } diff --git a/docs/releases/v1.24.0-next.0-changelog.md b/docs/releases/v1.24.0-next.0-changelog.md new file mode 100644 index 0000000000..c46cd38c26 --- /dev/null +++ b/docs/releases/v1.24.0-next.0-changelog.md @@ -0,0 +1,4281 @@ +# Release v1.24.0-next.0 + +## @backstage/backend-app-api@0.6.0-next.0 + +### Minor Changes + +- 4a3d434: **BREAKING**: For users that have migrated to the new backend system, incoming requests will now be rejected if they are not properly authenticated (e.g. with a Backstage bearer token or a backend token). Please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to circumvent this behavior in the short term and how to properly leverage it in the longer term. + + Added service factories for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 0502d82: Updated the `permissionsServiceFactory` to forward the `AuthService` to the implementation. +- 9802004: Made the `DefaultUserInfoService` claims check stricter +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend@0.22.0-next.0 + +### Minor Changes + +- 293c835: Add support for Service Tokens to Cloudflare Access auth provider +- 492fe83: **BREAKING**: The `CatalogIdentityClient` constructor now also requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support the new auth services, which has also been done for the `createRouter` function. + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 2af5354: Bump dependency `jose` to v5 +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- fa7ea3f: Internal refactor to break out how the router is constructed +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.8-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.1.3-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.8-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.1.5-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.6-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.0-next.0 + +### Minor Changes + +- 1bedb23: Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so, + + ```yaml title=app-config.yaml + auth: + providers: + guest: + userEntityRef: user:default/guest + ``` + + This also adds a new property to control the ownership entity refs, + + ```yaml title=app-config.yaml + auth: + providers: + guest: + ownershipEntityRefs: + - guests + - development/custom + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/plugin-azure-devops@0.4.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + +- a9e7bd6: **BREAKING** The `AzureDevOpsClient` no longer requires `identityAPi` but now requires `fetchApi`. + + Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops-common@0.4.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## @backstage/plugin-azure-devops-backend@0.6.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-azure-devops-common@0.4.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-azure-devops-common@0.4.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + +## @backstage/plugin-azure-sites-backend@0.3.0-next.0 + +### Minor Changes + +- 6b802a2: **BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- 85db926: Added new backend system for the Azure Sites backend plugin +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + +## @backstage/plugin-catalog-backend@1.18.0-next.0 + +### Minor Changes + +- df12231: Allow setting EntityDataParser using CatalogModelExtensionPoint +- 15ba00f: Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation. + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 280edeb: Add index for original value in search table for faster entity facet response +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/repo-tools@0.6.3-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.0-next.0 + +### Minor Changes + +- 9e527c9: BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support. + + `BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`. + With provided `events`, the event-based updates/refresh will be available. + However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`. + + The event subscription happens on `connect(connection)` if the `events` is available. + + **Migration:** + + ```diff + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, + }, + ); + - env.eventBroker.subscribe(bitbucketCloudProvider); + ``` + + **New Backend System:** + + Before this change, using this module with the new backend system was broken. + Now, you can add the catalog module for Bitbucket Cloud incl. event support backend. + Event support will always be enabled. + However, no updates/refresh will happen without receiving events. + + ```ts + backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), + ); + ``` + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.17-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-node@1.8.0-next.0 + +### Minor Changes + +- df12231: Allow setting EntityDataParser using CatalogModelExtensionPoint + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-devtools-backend@0.3.0-next.0 + +### Minor Changes + +- 4dc5b48: **BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.9-next.0 + +## @backstage/plugin-events-backend@0.3.0-next.0 + +### Minor Changes + +- c4bd794: BREAKING CHANGE: Migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService`. + + Uses the `EventsService` instead of `EventBroker` at `HttpPostIngressEventPublisher`, + dropping the use of `EventPublisher` including `setEventBroker(..)`. + + Now, `HttpPostIngressEventPublisher.fromConfig` requires `events: EventsService` as option. + + ```diff + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + + events: env.events, + logger: env.logger, + }); + http.bind(eventsRouter); + + // e.g. at packages/backend/src/plugins/events.ts + - await new EventsBackend(env.logger) + - .setEventBroker(env.eventBroker) + - .addPublishers(http) + - .start(); + + // or for other kinds of setups + - await Promise.all(http.map(publisher => publisher.setEventBroker(eventBroker))); + ``` + + `eventsPlugin` uses the `eventsServiceRef` as dependency. + Unsupported (and deprecated) extension point methods will throw an error to prevent unintended behavior. + + ```ts + import { eventsServiceRef } from '@backstage/plugin-events-node'; + ``` + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.0-next.0 + +### Minor Changes + +- 132d672: BREAKING CHANGE: Migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService`. + + Uses the `EventsService` instead of `EventBroker` at `AwsSqsConsumingEventPublisher`, + dropping the use of `EventPublisher` including `setEventBroker(..)`. + + Now, `AwsSqsConsumingEventPublisher.fromConfig` requires `events: EventsService` as option. + + ```diff + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, + + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); + + await Promise.all(sqs.map(publisher => publisher.start())); + + // e.g. at packages/backend/src/plugins/events.ts + - await new EventsBackend(env.logger) + - .setEventBroker(env.eventBroker) + - .addPublishers(sqs) + - .start(); + + // or for other kinds of setups + - await Promise.all(sqs.map(publisher => publisher.setEventBroker(eventBroker))); + ``` + + `eventsModuleAwsSqsConsumingEventPublisher` uses the `eventsServiceRef` as dependency, + instead of `eventsExtensionPoint`. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-events-backend-module-github@0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-events-node@0.3.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-jenkins-backend@0.4.0-next.0 + +### Minor Changes + +- 55191cc: **BREAKING**: Both `createRouter` and `DefaultJenkinsInfoProvider.fromConfig` now require the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + + The `JenkinsInfoProvider` interface has been updated to receive `credentials` of the type `BackstageCredentials` rather than a token. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-jenkins-common@0.1.25-next.0 + +## @backstage/plugin-kubernetes-backend@0.16.0-next.0 + +### Minor Changes + +- e1e540c: **BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications@0.1.0-next.0 + +### Minor Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + +### Patch Changes + +- 5d9c5ba: The Notifications can be newly filtered based on the Created Date. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.2-next.0 + +## @backstage/plugin-notifications-backend@0.1.0-next.0 + +### Minor Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + +### Patch Changes + +- 5d9c5ba: The Notifications can be newly filtered based on the Created Date. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 84af361: Migrated to using the new auth services. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-notifications-node@0.1.0-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-notifications-node@0.1.0-next.0 + +### Minor Changes + +- 84af361: Migrated to using the new auth services. + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/plugin-scaffolder-backend@1.22.0-next.0 + +### Minor Changes + +- c6b132e: Introducing checkpoints for scaffolder task action idempotency + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.5-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.5-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.3-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.0-next.0 + +### Minor Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.3.3-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-tech-insights-node@0.5.0-next.0 + +### Minor Changes + +- d621468: **BREAKING**: The `FactRetrieverContext` type now contains an additional `auth` field. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + +## @backstage/app-defaults@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/backend-common@0.21.3-next.0 + +### Patch Changes + +- 7422430: Resolve the `basePath` before constructing the target path + +- 999224f: Bump dependency `minimatch` to v9 + +- e0b997c: Fix issue where `resolveSafeChildPath` path would incorrectly resolve when operating on a symlink + +- 9802004: Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters` + +- 2af5354: Bump dependency `jose` to v5 + +- ff40ada: Updated dependency `mysql2` to `^3.0.0`. + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. + +- 568881f: Updated dependency `yauzl` to `^3.0.0`. + +- 4a3d434: Added a `createLegacyAuthAdapters` function that can be used as a compatibility adapter for backend plugins who want to start using the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). + + See the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on the usage of this adapter. + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.2.13-next.0 + +### Patch Changes + +- 7cbb760: Added support for the new auth services, which are now installed by default. See the [migration guide](https://backstage.io/docs/tutorials/auth-service-migration) for details. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + +## @backstage/backend-dynamic-feature-service@0.2.3-next.0 + +### Patch Changes + +- 5247909: Add `events: EventsService` to `LegacyPluginEnvironment`. +- Updated dependencies + - @backstage/plugin-events-backend@0.3.0-next.0 + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-app-node@0.1.13-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.6-next.0 + +### Patch Changes + +- 85ec23e: Updated dependency `json-schema-to-ts` to `^3.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/backend-plugin-api@0.6.13-next.0 + +### Patch Changes + +- 4a3d434: Added the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution) to the `coreServices`. + + At the same time, the [`httpRouter`](https://backstage.io/docs/backend-system/core-services/http-router) service gained a new `addAuthPolicy` method that lets your plugin declare exemptions to the default auth policy - for example if you want to allow unauthenticated or cookie-based access to some subset of your feature routes. + + If you have migrated to the new backend system, please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to move toward using these services. + +- 0502d82: Updated the `PermissionsService` methods to accept `BackstageCredentials` through options. + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/backend-tasks@0.5.18-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.3.3-next.0 + +### Patch Changes + +- 4a3d434: Added support for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). These services will be present by default in test apps, and you can access mocked versions of their features under `mockServices.auth` and `mockServices.httpAuth` if you want to inspect or replace their behaviors. + + There is also a new `mockCredentials` that you can use for acquiring mocks of the various types of credentials that are used in the new system. + +- 9802004: Added `mockServices.userInfo`, which now also automatically is made available in test backends. + +- fd61d39: Updated dependency `testcontainers` to `^10.0.0`. + +- ff40ada: Updated dependency `mysql2` to `^3.0.0`. + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/catalog-client@1.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/catalog-model@1.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## @backstage/cli@0.25.3-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- f86e34c: Removed unused `replace-in-file` dependency +- f4404e5: Add .ico import support +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/eslint-plugin@0.1.6-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/cli-node@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## @backstage/config@1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## @backstage/config-loader@1.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/core-compat-api@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/version-bridge@1.0.7 + +## @backstage/core-components@0.14.1-next.0 + +### Patch Changes + +- ff33ee2: Removed hardcoded font-family on select input +- ff7e126: Support i18n for core components +- 7854120: Create a component abstraction to consume system icons. +- ce73c3b: Removed the inline color from select icon to allow it to be colored via a theme +- a8f7904: `SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/version-bridge@1.0.7 + +## @backstage/core-plugin-api@1.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/create-app@0.5.12-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.13 + +## @backstage/dev-utils@1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + +## @backstage/errors@1.2.4-next.0 + +### Patch Changes + +- 2636075: Fixed an issue that was causing ResponseError not to report the HTTP status from the provided response. +- Updated dependencies + - @backstage/types@1.1.1 + +## @backstage/eslint-plugin@0.1.6-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 + +## @backstage/frontend-app-api@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/frontend-plugin-api@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/frontend-test-utils@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/integration@1.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/integration-aws-node@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/integration-react@1.1.25-next.0 + +### Patch Changes + +- b38dc55: Updated `microsoftAuthApi` scopes for Azure DevOps to be fully qualified. +- Updated dependencies + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/repo-tools@0.6.3-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/cli-common@0.1.13 + +## @techdocs/cli@1.8.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + +## @backstage/test-utils@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/theme@0.5.2-next.0 + +### Patch Changes + +- 6f4d2a0: Exported `defaultTypography` to make adjusting these values in a custom theme easier + +## @backstage/plugin-adr@0.6.14-next.0 + +### Patch Changes + +- 5335634: Fixed Azure DevOps ADR file path reading +- 669efc6: Remove unused package dependencies +- Updated dependencies + - @backstage/plugin-adr-common@0.2.21-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-adr-backend@0.4.10-next.0 + +### Patch Changes + +- 334c5fe: Updated dependency `marked` to `^12.0.0`. +- c8fdd83: Migrated `DefaultAdrCollatorFactory` to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-adr-common@0.2.21-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-adr-common@0.2.21-next.0 + +### Patch Changes + +- 5335634: Fixed Azure DevOps ADR file path reading +- Updated dependencies + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-airbrake@0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/dev-utils@1.0.28-next.0 + - @backstage/test-utils@1.5.1-next.0 + +## @backstage/plugin-airbrake-backend@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-allure@0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-analytics-module-ga@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-analytics-module-ga4@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-analytics-module-newrelic-browser@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-apache-airflow@0.2.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-api-docs@0.11.1-next.0 + +### Patch Changes + +- 7854120: Use the `AppIcon` component in the navigation item extension. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/plugin-apollo-explorer@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-app-backend@0.3.61-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-app-node@0.1.13-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config-loader@1.6.3-next.0 + +## @backstage/plugin-app-visualizer@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.4-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.8-next.0 + +### Patch Changes + +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.10-next.0 + +### Patch Changes + +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.8-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.6-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- e77d7a9: Internal refactor to avoid deprecated method. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.1.3-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.7-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.1.5-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/plugin-auth-node@0.4.8-next.0 + +### Patch Changes + +- b4fc6e3: Deprecated the `getBearerTokenFromAuthorizationHeader` function, which is being replaced by the new `HttpAuthService`. +- 2af5354: Bump dependency `jose` to v5 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-azure-sites@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/plugin-azure-sites-common@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-badges@0.2.55-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-badges-backend@0.3.10-next.0 + +### Patch Changes + +- 29a1f91: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-bazaar@0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-bazaar-backend@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-bitbucket-cloud-common@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-bitrise@0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-catalog@1.17.1-next.0 + +### Patch Changes + +- 9332425: The entity page extension provided by the `/alpha` plugin now correctly renders the entity 404 page. +- 6727665: Allow the `spec.target` field to be searchable in the catalog table for locations. Previously, only the `spec.targets` field was be searchable. This makes locations generated by providers such as the `GithubEntityProvider` searchable in the catalog table. [#23098](https://github.com/backstage/backstage/issues/23098) +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.3.7-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.32-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.1.6-next.0 + +### Patch Changes + +- 43a9ae1: Migrated to use new auth service. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.26-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.29-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.5.3-next.0 + +### Patch Changes + +- a936a8f: Migrated the `GithubLocationAnalyzer` to support new auth services. +- 999224f: Bump dependency `minimatch` to v9 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-catalog-backend-module-github@0.5.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.10-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.17-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.28-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.20-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.18-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/plugin-catalog-common@1.0.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## @backstage/plugin-catalog-graph@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.10.7-next.0 + +### Patch Changes + +- 75f686b: Fixed an issue generating a wrong entity link at the end of the import process +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-catalog-react@1.10.1-next.0 + +### Patch Changes + +- 930b5c1: Added 'root' and 'label' class keys for EntityAutocompletePicker, EntityOwnerPicker and EntityProcessingStatusPicker +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-cicd-statistics@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.27-next.0 + +### Patch Changes + +- 402d991: Align `p-limit` dependency version to v3 +- Updated dependencies + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-cicd-statistics@0.1.33-next.0 + +## @backstage/plugin-circleci@0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-cloudbuild@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-code-climate@0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-code-coverage@0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-code-coverage-backend@0.2.27-next.0 + +### Patch Changes + +- cceebae: Fix jacoco convertor to not require annotation to be set to scm-only. +- 8efe690: Migrated to support new auth services. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-codescene@0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-config-schema@0.1.51-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-cost-insights@0.12.20-next.0 + +### Patch Changes + +- 1b4fd09: Updated dependency `yup` to `^1.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-cost-insights-common@0.1.2 + +## @backstage/plugin-devtools@0.1.10-next.0 + +### Patch Changes + +- a0e3393: Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-devtools-common@0.1.9-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## @backstage/plugin-devtools-common@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-dynatrace@9.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-entity-feedback@0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-entity-feedback-common@0.1.3 + +## @backstage/plugin-entity-feedback-backend@0.2.10-next.0 + +### Patch Changes + +- 4f8ecd6: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-entity-feedback-common@0.1.3 + +## @backstage/plugin-entity-validation@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.23-next.0 + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + +## @backstage/plugin-explore@0.4.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/plugin-explore-common@0.0.2 + - @backstage/plugin-explore-react@0.0.37-next.0 + +## @backstage/plugin-explore-backend@0.0.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-explore-common@0.0.2 + +## @backstage/plugin-explore-react@0.0.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-explore-common@0.0.2 + +## @backstage/plugin-firehydrant@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-fossa@0.2.63-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-gcalendar@0.3.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-gcp-projects@0.3.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-git-release-manager@0.3.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-github-actions@0.6.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-github-deployments@0.1.62-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-github-issues@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-github-pull-requests-board@0.1.25-next.0 + +### Patch Changes + +- 3c2d7c0: The `CardHeader` component in the `github-pull-requests-board` plugin will show the status for the PR +- 402d991: Align `p-limit` dependency version to v3 +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-gitops-profiles@0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-gocd@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-graphiql@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## @backstage/plugin-graphql-voyager@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-home@0.6.3-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## @backstage/plugin-home-react@0.1.9-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-ilert@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-jenkins@0.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-jenkins-common@0.1.25-next.0 + +## @backstage/plugin-jenkins-common@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-kafka@0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-kafka-backend@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-kubernetes@0.11.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-react@0.3.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-react@0.3.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-kubernetes-common@0.7.5-next.0 + +### Patch Changes + +- 4642cb7: Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.3.1-next.0 + +### Patch Changes + +- 4642cb7: Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-lighthouse@0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-lighthouse-common@0.1.5-next.0 + +## @backstage/plugin-lighthouse-backend@0.4.5-next.0 + +### Patch Changes + +- 9f9ba70: **BREAKING**: The `createScheduler` function now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-lighthouse-common@0.1.5-next.0 + +## @backstage/plugin-lighthouse-common@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-linguist@0.1.16-next.0 + +### Patch Changes + +- 4fb9600: Get component's title from translation file. See: +- a0e3393: Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) +- 786c9c4: Updated dependency `luxon` to `^3.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-linguist-common@0.1.2 + +## @backstage/plugin-linguist-backend@0.5.10-next.0 + +### Patch Changes + +- 61ff58f: Migrated to support new auth services. +- 786c9c4: Updated dependency `luxon` to `^3.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-linguist-common@0.1.2 + +## @backstage/plugin-microsoft-calendar@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-newrelic@0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-newrelic-dashboard@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-nomad@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-nomad-backend@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-notifications-common@0.0.2-next.0 + +### Patch Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + +## @backstage/plugin-octopus-deploy@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-opencost@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-org@0.6.21-next.0 + +### Patch Changes + +- 526f00a: Document the new frontend system extensions for the org plugin. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-org-react@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-pagerduty@0.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-periskop@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-periskop-backend@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-permission-backend@0.5.36-next.0 + +### Patch Changes + +- 9802004: Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md). + + The `createRouter` function now accepts `auth`, `httpAuth` and `userInfo` options. Theses are used internally to support the new backend system, and can be ignored. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + +## @backstage/plugin-permission-common@0.7.13-next.0 + +### Patch Changes + +- 0502d82: The `token` option of the `PermissionEvaluator` methods is now deprecated. The options that only apply to backend implementations have been moved to `PermissionsService` from `@backstage/backend-plugin-api` instead. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.7.24-next.0 + +### Patch Changes + +- 0502d82: The `ServerPermissionClient` has been migrated to implement the `PermissionsService` interface, now accepting the new `BackstageCredentials` object in addition to the `token` option, which is now deprecated. It now also optionally depends on the new `AuthService`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-permission-react@0.4.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-playlist@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-playlist-common@0.1.15-next.0 + +## @backstage/plugin-playlist-backend@0.3.17-next.0 + +### Patch Changes + +- 6813366: Migrated to support new auth services. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-playlist-common@0.1.15-next.0 + +## @backstage/plugin-playlist-common@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + +## @backstage/plugin-proxy-backend@0.4.11-next.0 + +### Patch Changes + +- 1b4fd09: Updated dependency `yup` to `^1.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-puppetdb@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-rollbar@0.4.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-rollbar-backend@0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-scaffolder@1.18.1-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.5-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.14-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.37-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.5-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- 1753898: Updated dependency `octokit-plugin-create-pull-request` to `^5.0.0`. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.30-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.21-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.34-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.0-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-common@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.3.3-next.0 + +### Patch Changes + +- 85f4723: Fixed file corruption for non UTF-8 data in fetch contents +- c6b132e: Introducing checkpoints for scaffolder task action idempotency +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-scaffolder-react@1.8.1-next.0 + +### Patch Changes + +- 930b5c1: Added 'root' and 'label' class key to TemplateCategoryPicker +- 6d649d2: Updated dependency `flatted` to `3.3.1`. +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + +## @backstage/plugin-search@1.4.7-next.0 + +### Patch Changes + +- f0464b0: Removes ADR from the default set of search filters +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/plugin-search-backend@1.5.3-next.0 + +### Patch Changes + +- 744c0cb: Update the router to use the new `auth` services, it now accepts an optional discovery service option to get credentials for the permission service. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.16-next.0 + +### Patch Changes + +- 744c0cb: Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + +## @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-explore-common@0.0.2 + +## @backstage/plugin-search-backend-module-pg@0.5.22-next.0 + +### Patch Changes + +- 744c0cb: Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-search-backend-node@1.2.17-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 744c0cb: Exports `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` types. These new types were extracted from the `@backstage/plugin-search-common` package and the `token` property was deprecated in favor of the a new credentials one. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-search-common@1.2.11-next.0 + +### Patch Changes + +- 744c0cb: Deprecate `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` in favor of the types exported from `@backstage/plugin-search-backend-node`. +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + +## @backstage/plugin-sentry@0.5.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-shortcuts@0.3.20-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals@0.0.2-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.2-next.0 + +## @backstage/plugin-signals-backend@0.0.4-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.0.4-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-react@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-sonarqube@0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-sonarqube-react@0.1.14-next.0 + +## @backstage/plugin-sonarqube-backend@0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + +## @backstage/plugin-sonarqube-react@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-splunk-on-call@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-stack-overflow@0.1.26-next.0 + +### Patch Changes + +- c6779ac: fix: fix decode issues in title and author fields in `StackOverflowSearchResultListItem` +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## @backstage/plugin-stack-overflow-backend@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.6-next.0 + +## @backstage/plugin-stackstorm@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-tech-insights@0.3.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + +## @backstage/plugin-tech-insights-backend@0.5.27-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- d621468: Added support for the new `AuthService`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + +## @backstage/plugin-tech-radar@0.6.14-next.0 + +### Patch Changes + +- a2327ac: Fixed an issue with the "moved in direction" table header cell getting squished and becoming unreadable if a timeline description is too long +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## @backstage/plugin-techdocs@1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/core-app-api@1.12.1-next.0 + +## @backstage/plugin-techdocs-backend@1.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + +## @backstage/plugin-techdocs-node@1.11.5-next.0 + +### Patch Changes + +- 5b4f565: Fix handling of default plugins that have configuration +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + +## @backstage/plugin-techdocs-react@1.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/version-bridge@1.0.7 + +## @backstage/plugin-todo@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-todo-backend@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/repo-tools@0.6.3-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + +## @backstage/plugin-user-settings@0.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-user-settings-backend@0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-vault@0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @backstage/plugin-vault-backend@0.4.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-vault-node@0.1.6-next.0 + +## @backstage/plugin-vault-node@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @backstage/plugin-xcmetrics@0.2.49-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## example-app@0.2.93-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-github-pull-requests-board@0.1.25-next.0 + - @backstage/plugin-adr@0.6.14-next.0 + - @backstage/plugin-stack-overflow@0.1.26-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-notifications@0.1.0-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops@0.4.0-next.0 + - @backstage/plugin-linguist@0.1.16-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/plugin-org@0.6.21-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search@1.4.7-next.0 + - @backstage/plugin-devtools@0.1.10-next.0 + - @backstage/plugin-tech-radar@0.6.14-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/plugin-api-docs@0.11.1-next.0 + - @backstage/plugin-cost-insights@0.12.20-next.0 + - @backstage/plugin-home@0.6.3-next.0 + - @backstage/plugin-scaffolder@1.18.1-next.0 + - @backstage/plugin-shortcuts@0.3.20-next.0 + - @backstage/plugin-signals@0.0.2-next.0 + - @backstage/plugin-catalog-import@0.10.7-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/plugin-badges@0.2.55-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0 + - @backstage/plugin-code-coverage@0.2.24-next.0 + - @backstage/plugin-entity-feedback@0.2.14-next.0 + - @backstage/plugin-explore@0.4.17-next.0 + - @backstage/plugin-gcalendar@0.3.24-next.0 + - @backstage/plugin-gocd@0.1.37-next.0 + - @backstage/plugin-jenkins@0.9.6-next.0 + - @backstage/plugin-microsoft-calendar@0.1.13-next.0 + - @backstage/plugin-newrelic-dashboard@0.3.6-next.0 + - @backstage/plugin-pagerduty@0.7.3-next.0 + - @backstage/plugin-playlist@0.2.5-next.0 + - @backstage/plugin-puppetdb@0.1.14-next.0 + - @backstage/plugin-stackstorm@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.23-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/plugin-todo@0.2.35-next.0 + - @backstage/plugin-user-settings@0.8.2-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/plugin-azure-sites@0.1.20-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/plugin-airbrake@0.3.31-next.0 + - @backstage/plugin-apache-airflow@0.2.21-next.0 + - @backstage/plugin-catalog-graph@0.4.1-next.0 + - @backstage/plugin-cloudbuild@0.4.1-next.0 + - @backstage/plugin-dynatrace@9.0.1-next.0 + - @backstage/plugin-gcp-projects@0.3.47-next.0 + - @backstage/plugin-github-actions@0.6.12-next.0 + - @backstage/plugin-graphiql@0.3.4-next.0 + - @backstage/plugin-kafka@0.3.31-next.0 + - @backstage/plugin-kubernetes@0.11.6-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.7-next.0 + - @backstage/plugin-lighthouse@0.4.16-next.0 + - @backstage/plugin-newrelic@0.3.46-next.0 + - @backstage/plugin-nomad@0.1.12-next.0 + - @backstage/plugin-octopus-deploy@0.2.13-next.0 + - @backstage/plugin-rollbar@0.4.31-next.0 + - @backstage/plugin-sentry@0.5.16-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-linguist-common@0.1.2 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## example-app-next@0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-adr@0.6.14-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops@0.4.0-next.0 + - @backstage/plugin-linguist@0.1.16-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/plugin-org@0.6.21-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search@1.4.7-next.0 + - @backstage/plugin-devtools@0.1.10-next.0 + - @backstage/plugin-tech-radar@0.6.14-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/plugin-api-docs@0.11.1-next.0 + - @backstage/plugin-cost-insights@0.12.20-next.0 + - @backstage/plugin-home@0.6.3-next.0 + - @backstage/plugin-scaffolder@1.18.1-next.0 + - @backstage/plugin-shortcuts@0.3.20-next.0 + - @backstage/plugin-catalog-import@0.10.7-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/plugin-badges@0.2.55-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0 + - @backstage/plugin-code-coverage@0.2.24-next.0 + - @backstage/plugin-entity-feedback@0.2.14-next.0 + - @backstage/plugin-explore@0.4.17-next.0 + - @backstage/plugin-gcalendar@0.3.24-next.0 + - @backstage/plugin-gocd@0.1.37-next.0 + - @backstage/plugin-jenkins@0.9.6-next.0 + - @backstage/plugin-microsoft-calendar@0.1.13-next.0 + - @backstage/plugin-newrelic-dashboard@0.3.6-next.0 + - @backstage/plugin-pagerduty@0.7.3-next.0 + - @backstage/plugin-playlist@0.2.5-next.0 + - @backstage/plugin-puppetdb@0.1.14-next.0 + - @backstage/plugin-stackstorm@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.23-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/plugin-todo@0.2.35-next.0 + - @backstage/plugin-user-settings@0.8.2-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/plugin-azure-sites@0.1.20-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - app-next-example-plugin@0.0.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/plugin-airbrake@0.3.31-next.0 + - @backstage/plugin-apache-airflow@0.2.21-next.0 + - @backstage/plugin-app-visualizer@0.1.2-next.0 + - @backstage/plugin-catalog-graph@0.4.1-next.0 + - @backstage/plugin-cloudbuild@0.4.1-next.0 + - @backstage/plugin-dynatrace@9.0.1-next.0 + - @backstage/plugin-gcp-projects@0.3.47-next.0 + - @backstage/plugin-github-actions@0.6.12-next.0 + - @backstage/plugin-graphiql@0.3.4-next.0 + - @backstage/plugin-kafka@0.3.31-next.0 + - @backstage/plugin-kubernetes@0.11.6-next.0 + - @backstage/plugin-lighthouse@0.4.16-next.0 + - @backstage/plugin-newrelic@0.3.46-next.0 + - @backstage/plugin-octopus-deploy@0.2.13-next.0 + - @backstage/plugin-rollbar@0.4.31-next.0 + - @backstage/plugin-sentry@0.5.16-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-linguist-common@0.1.2 + - @backstage/plugin-permission-react@0.4.21-next.0 + +## app-next-example-plugin@0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + +## example-backend@0.2.93-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-backend@0.3.0-next.0 + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.5.10-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-lighthouse-backend@0.4.5-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.16-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.22-next.0 + - @backstage/plugin-playlist-backend@0.3.17-next.0 + - @backstage/plugin-code-coverage-backend@0.2.27-next.0 + - @backstage/plugin-entity-feedback-backend@0.2.10-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + - @backstage/plugin-jenkins-backend@0.4.0-next.0 + - @backstage/plugin-azure-devops-backend@0.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.14-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.30-next.0 + - @backstage/plugin-scaffolder-backend@1.22.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-kubernetes-backend@0.16.0-next.0 + - @backstage/plugin-adr-backend@0.4.10-next.0 + - @backstage/plugin-proxy-backend@0.4.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-signals-backend@0.0.4-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-tech-insights-backend@0.5.27-next.0 + - @backstage/plugin-search-backend@1.5.3-next.0 + - @backstage/plugin-devtools-backend@0.3.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/plugin-badges-backend@0.3.10-next.0 + - @backstage/plugin-permission-backend@0.5.36-next.0 + - @backstage/plugin-app-backend@0.3.61-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/plugin-explore-backend@0.0.23-next.0 + - @backstage/plugin-rollbar-backend@0.1.58-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.45-next.0 + - @backstage/plugin-techdocs-backend@1.9.6-next.0 + - @backstage/plugin-kafka-backend@0.3.11-next.0 + - @backstage/plugin-nomad-backend@0.1.15-next.0 + - @backstage/plugin-todo-backend@0.3.11-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0 + - example-app@0.2.93-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + +## example-backend-next@0.0.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-backend@0.5.10-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-lighthouse-backend@0.4.5-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.0-next.0 + - @backstage/plugin-playlist-backend@0.3.17-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-entity-feedback-backend@0.2.10-next.0 + - @backstage/plugin-notifications-backend@0.1.0-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + - @backstage/plugin-jenkins-backend@0.4.0-next.0 + - @backstage/plugin-azure-devops-backend@0.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend@1.22.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/backend-defaults@0.2.13-next.0 + - @backstage/plugin-kubernetes-backend@0.16.0-next.0 + - @backstage/plugin-adr-backend@0.4.10-next.0 + - @backstage/plugin-proxy-backend@0.4.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-signals-backend@0.0.4-next.0 + - @backstage/plugin-search-backend@1.5.3-next.0 + - @backstage/plugin-devtools-backend@0.3.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.1.6-next.0 + - @backstage/plugin-badges-backend@0.3.10-next.0 + - @backstage/plugin-permission-backend@0.5.36-next.0 + - @backstage/plugin-app-backend@0.3.61-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.30-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.10-next.0 + - @backstage/plugin-sonarqube-backend@0.2.15-next.0 + - @backstage/plugin-techdocs-backend@1.9.6-next.0 + - @backstage/plugin-nomad-backend@0.1.15-next.0 + - @backstage/plugin-todo-backend@0.3.11-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0 + - @backstage/catalog-model@1.4.5-next.0 + +## e2e-test@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.12-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-common@0.1.13 + +## techdocs-cli-embedded-app@0.2.92-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/core-app-api@1.12.1-next.0 + +## @internal/plugin-todo-list@1.0.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + +## @internal/plugin-todo-list-backend@1.0.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + +## @internal/plugin-todo-list-common@1.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 diff --git a/package.json b/package.json index 937ea4e19d..f4694c05fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.23.0", + "version": "1.24.0-next.0", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 20a33c612d..7a81f55437 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 1.5.0 ### Minor Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 3b695129e1..9a31e5bd88 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.0", + "version": "1.5.1-next.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 29b7877580..444e8e8197 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.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.0.6 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 08a08f9603..aa2560bb46 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.6", + "version": "0.0.7-next.0", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin" diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 89378b491d..eb6c22934d 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,81 @@ # example-app-next +## 0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-adr@0.6.14-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops@0.4.0-next.0 + - @backstage/plugin-linguist@0.1.16-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/plugin-org@0.6.21-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search@1.4.7-next.0 + - @backstage/plugin-devtools@0.1.10-next.0 + - @backstage/plugin-tech-radar@0.6.14-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/plugin-api-docs@0.11.1-next.0 + - @backstage/plugin-cost-insights@0.12.20-next.0 + - @backstage/plugin-home@0.6.3-next.0 + - @backstage/plugin-scaffolder@1.18.1-next.0 + - @backstage/plugin-shortcuts@0.3.20-next.0 + - @backstage/plugin-catalog-import@0.10.7-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/plugin-badges@0.2.55-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0 + - @backstage/plugin-code-coverage@0.2.24-next.0 + - @backstage/plugin-entity-feedback@0.2.14-next.0 + - @backstage/plugin-explore@0.4.17-next.0 + - @backstage/plugin-gcalendar@0.3.24-next.0 + - @backstage/plugin-gocd@0.1.37-next.0 + - @backstage/plugin-jenkins@0.9.6-next.0 + - @backstage/plugin-microsoft-calendar@0.1.13-next.0 + - @backstage/plugin-newrelic-dashboard@0.3.6-next.0 + - @backstage/plugin-pagerduty@0.7.3-next.0 + - @backstage/plugin-playlist@0.2.5-next.0 + - @backstage/plugin-puppetdb@0.1.14-next.0 + - @backstage/plugin-stackstorm@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.23-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/plugin-todo@0.2.35-next.0 + - @backstage/plugin-user-settings@0.8.2-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/plugin-azure-sites@0.1.20-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - app-next-example-plugin@0.0.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/plugin-airbrake@0.3.31-next.0 + - @backstage/plugin-apache-airflow@0.2.21-next.0 + - @backstage/plugin-app-visualizer@0.1.2-next.0 + - @backstage/plugin-catalog-graph@0.4.1-next.0 + - @backstage/plugin-cloudbuild@0.4.1-next.0 + - @backstage/plugin-dynatrace@9.0.1-next.0 + - @backstage/plugin-gcp-projects@0.3.47-next.0 + - @backstage/plugin-github-actions@0.6.12-next.0 + - @backstage/plugin-graphiql@0.3.4-next.0 + - @backstage/plugin-kafka@0.3.31-next.0 + - @backstage/plugin-kubernetes@0.11.6-next.0 + - @backstage/plugin-lighthouse@0.4.16-next.0 + - @backstage/plugin-newrelic@0.3.46-next.0 + - @backstage/plugin-octopus-deploy@0.2.13-next.0 + - @backstage/plugin-rollbar@0.4.31-next.0 + - @backstage/plugin-sentry@0.5.16-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-linguist-common@0.1.2 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 0.0.6 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index bd3db0d1c9..39457f8cf7 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.6", + "version": "0.0.7-next.0", "private": true, "repository": { "type": "git", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 766214cb2d..149d101dd9 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,84 @@ # example-app +## 0.2.93-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-github-pull-requests-board@0.1.25-next.0 + - @backstage/plugin-adr@0.6.14-next.0 + - @backstage/plugin-stack-overflow@0.1.26-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-notifications@0.1.0-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops@0.4.0-next.0 + - @backstage/plugin-linguist@0.1.16-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/plugin-org@0.6.21-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search@1.4.7-next.0 + - @backstage/plugin-devtools@0.1.10-next.0 + - @backstage/plugin-tech-radar@0.6.14-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/plugin-api-docs@0.11.1-next.0 + - @backstage/plugin-cost-insights@0.12.20-next.0 + - @backstage/plugin-home@0.6.3-next.0 + - @backstage/plugin-scaffolder@1.18.1-next.0 + - @backstage/plugin-shortcuts@0.3.20-next.0 + - @backstage/plugin-signals@0.0.2-next.0 + - @backstage/plugin-catalog-import@0.10.7-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/plugin-badges@0.2.55-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.1.9-next.0 + - @backstage/plugin-code-coverage@0.2.24-next.0 + - @backstage/plugin-entity-feedback@0.2.14-next.0 + - @backstage/plugin-explore@0.4.17-next.0 + - @backstage/plugin-gcalendar@0.3.24-next.0 + - @backstage/plugin-gocd@0.1.37-next.0 + - @backstage/plugin-jenkins@0.9.6-next.0 + - @backstage/plugin-microsoft-calendar@0.1.13-next.0 + - @backstage/plugin-newrelic-dashboard@0.3.6-next.0 + - @backstage/plugin-pagerduty@0.7.3-next.0 + - @backstage/plugin-playlist@0.2.5-next.0 + - @backstage/plugin-puppetdb@0.1.14-next.0 + - @backstage/plugin-stackstorm@0.1.12-next.0 + - @backstage/plugin-tech-insights@0.3.23-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/plugin-todo@0.2.35-next.0 + - @backstage/plugin-user-settings@0.8.2-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/plugin-azure-sites@0.1.20-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/plugin-airbrake@0.3.31-next.0 + - @backstage/plugin-apache-airflow@0.2.21-next.0 + - @backstage/plugin-catalog-graph@0.4.1-next.0 + - @backstage/plugin-cloudbuild@0.4.1-next.0 + - @backstage/plugin-dynatrace@9.0.1-next.0 + - @backstage/plugin-gcp-projects@0.3.47-next.0 + - @backstage/plugin-github-actions@0.6.12-next.0 + - @backstage/plugin-graphiql@0.3.4-next.0 + - @backstage/plugin-kafka@0.3.31-next.0 + - @backstage/plugin-kubernetes@0.11.6-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.7-next.0 + - @backstage/plugin-lighthouse@0.4.16-next.0 + - @backstage/plugin-newrelic@0.3.46-next.0 + - @backstage/plugin-nomad@0.1.12-next.0 + - @backstage/plugin-octopus-deploy@0.2.13-next.0 + - @backstage/plugin-rollbar@0.4.31-next.0 + - @backstage/plugin-sentry@0.5.16-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.6-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-linguist-common@0.1.2 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 0.2.92 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 7936db0090..c3aa748b58 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.92", + "version": "0.2.93-next.0", "private": true, "backstage": { "role": "frontend" diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index c3ecdc3ed2..a52b2b0d86 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/backend-app-api +## 0.6.0-next.0 + +### Minor Changes + +- 4a3d434: **BREAKING**: For users that have migrated to the new backend system, incoming requests will now be rejected if they are not properly authenticated (e.g. with a Backstage bearer token or a backend token). Please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to circumvent this behavior in the short term and how to properly leverage it in the longer term. + + Added service factories for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 0502d82: Updated the `permissionsServiceFactory` to forward the `AuthService` to the implementation. +- 9802004: Made the `DefaultUserInfoService` claims check stricter +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + ## 0.5.11 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index ae46e13083..ecc4dce4b5 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-app-api", "description": "Core API used by Backstage backend apps", - "version": "0.5.11", + "version": "0.6.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 729bd13779..cc5714772e 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/backend-common +## 0.21.3-next.0 + +### Patch Changes + +- 7422430: Resolve the `basePath` before constructing the target path +- 999224f: Bump dependency `minimatch` to v9 +- e0b997c: Fix issue where `resolveSafeChildPath` path would incorrectly resolve when operating on a symlink +- 9802004: Added the `UserInfoApi` as both an optional input and as an output for `createLegacyAuthAdapters` +- 2af5354: Bump dependency `jose` to v5 +- ff40ada: Updated dependency `mysql2` to `^3.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 568881f: Updated dependency `yauzl` to `^3.0.0`. +- 4a3d434: Added a `createLegacyAuthAdapters` function that can be used as a compatibility adapter for backend plugins who want to start using the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). + + See the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on the usage of this adapter. + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + ## 0.21.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index f60502ecd1..26b00e1875 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.21.0", + "version": "0.21.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 964410d019..c6ce10a1bb 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-defaults +## 0.2.13-next.0 + +### Patch Changes + +- 7cbb760: Added support for the new auth services, which are now installed by default. See the [migration guide](https://backstage.io/docs/tutorials/auth-service-migration) for details. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + ## 0.2.10 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 05de926b61..da93e5e5a8 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-defaults", "description": "Backend defaults used by Backstage backend apps", - "version": "0.2.10", + "version": "0.2.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index bd924a3b39..783fd68c17 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/backend-dynamic-feature-service +## 0.2.3-next.0 + +### Patch Changes + +- 5247909: Add `events: EventsService` to `LegacyPluginEnvironment`. +- Updated dependencies + - @backstage/plugin-events-backend@0.3.0-next.0 + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-app-node@0.1.13-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 5adbd76afa..e47da0cb05 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.0", + "version": "0.2.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index 58625674b7..658f7b6b38 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,53 @@ # example-backend-next +## 0.0.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-backend@0.5.10-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-lighthouse-backend@0.4.5-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.0-next.0 + - @backstage/plugin-playlist-backend@0.3.17-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-entity-feedback-backend@0.2.10-next.0 + - @backstage/plugin-notifications-backend@0.1.0-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + - @backstage/plugin-jenkins-backend@0.4.0-next.0 + - @backstage/plugin-azure-devops-backend@0.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend@1.22.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/backend-defaults@0.2.13-next.0 + - @backstage/plugin-kubernetes-backend@0.16.0-next.0 + - @backstage/plugin-adr-backend@0.4.10-next.0 + - @backstage/plugin-proxy-backend@0.4.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-signals-backend@0.0.4-next.0 + - @backstage/plugin-search-backend@1.5.3-next.0 + - @backstage/plugin-devtools-backend@0.3.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.1.6-next.0 + - @backstage/plugin-badges-backend@0.3.10-next.0 + - @backstage/plugin-permission-backend@0.5.36-next.0 + - @backstage/plugin-app-backend@0.3.61-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.30-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.10-next.0 + - @backstage/plugin-sonarqube-backend@0.2.15-next.0 + - @backstage/plugin-techdocs-backend@1.9.6-next.0 + - @backstage/plugin-nomad-backend@0.1.15-next.0 + - @backstage/plugin-todo-backend@0.3.11-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 0.0.20 ### Patch Changes diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 49e3df2db9..610a46ebf6 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.20", + "version": "0.0.21-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 77c2426beb..660eec4f97 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-openapi-utils +## 0.1.6-next.0 + +### Patch Changes + +- 85ec23e: Updated dependency `json-schema-to-ts` to `^3.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.3 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index fbb670149f..284216f78f 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.1.3", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index c927ae0803..416bc7e2d2 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/backend-plugin-api +## 0.6.13-next.0 + +### Patch Changes + +- 4a3d434: Added the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/), [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth), and [`userInfo`](https://backstage.io/docs/backend-system/core-services/user-info) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution) to the `coreServices`. + + At the same time, the [`httpRouter`](https://backstage.io/docs/backend-system/core-services/http-router) service gained a new `addAuthPolicy` method that lets your plugin declare exemptions to the default auth policy - for example if you want to allow unauthenticated or cookie-based access to some subset of your feature routes. + + If you have migrated to the new backend system, please see the [Auth Service Migration tutorial](https://backstage.io/docs/tutorials/auth-service-migration) for more information on how to move toward using these services. + +- 0502d82: Updated the `PermissionsService` methods to accept `BackstageCredentials` through options. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.6.10 ### Patch Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index ae0037321b..4dd6fd3fcd 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-plugin-api", "description": "Core API used by Backstage backend plugins", - "version": "0.6.10", + "version": "0.6.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 53b8f4acca..22d2ea900b 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/backend-tasks +## 0.5.18-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.5.15 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 9f6743fe37..510dbae55b 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.15", + "version": "0.5.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 61c1a98005..f1da33e64c 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/backend-test-utils +## 0.3.3-next.0 + +### Patch Changes + +- 4a3d434: Added support for the new [`auth`](https://backstage.io/docs/backend-system/core-services/auth/) and [`httpAuth`](https://backstage.io/docs/backend-system/core-services/http-auth) services that were created as part of [BEP-0003](https://github.com/backstage/backstage/tree/master/beps/0003-auth-architecture-evolution). These services will be present by default in test apps, and you can access mocked versions of their features under `mockServices.auth` and `mockServices.httpAuth` if you want to inspect or replace their behaviors. + + There is also a new `mockCredentials` that you can use for acquiring mocks of the various types of credentials that are used in the new system. + +- 9802004: Added `mockServices.userInfo`, which now also automatically is made available in test backends. +- fd61d39: Updated dependency `testcontainers` to `^10.0.0`. +- ff40ada: Updated dependency `mysql2` to `^3.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-app-api@0.6.0-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 801b7c125b..d537adfba2 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": "0.3.0", + "version": "0.3.3-next.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 68e6c7c803..4ebc76804b 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,65 @@ # example-backend +## 0.2.93-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-backend@0.3.0-next.0 + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.5.10-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/plugin-lighthouse-backend@0.4.5-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.16-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.22-next.0 + - @backstage/plugin-playlist-backend@0.3.17-next.0 + - @backstage/plugin-code-coverage-backend@0.2.27-next.0 + - @backstage/plugin-entity-feedback-backend@0.2.10-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + - @backstage/plugin-jenkins-backend@0.4.0-next.0 + - @backstage/plugin-azure-devops-backend@0.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.14-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.30-next.0 + - @backstage/plugin-scaffolder-backend@1.22.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-kubernetes-backend@0.16.0-next.0 + - @backstage/plugin-adr-backend@0.4.10-next.0 + - @backstage/plugin-proxy-backend@0.4.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-signals-backend@0.0.4-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-tech-insights-backend@0.5.27-next.0 + - @backstage/plugin-search-backend@1.5.3-next.0 + - @backstage/plugin-devtools-backend@0.3.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/plugin-badges-backend@0.3.10-next.0 + - @backstage/plugin-permission-backend@0.5.36-next.0 + - @backstage/plugin-app-backend@0.3.61-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/plugin-explore-backend@0.0.23-next.0 + - @backstage/plugin-rollbar-backend@0.1.58-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.45-next.0 + - @backstage/plugin-techdocs-backend@1.9.6-next.0 + - @backstage/plugin-kafka-backend@0.3.11-next.0 + - @backstage/plugin-nomad-backend@0.1.15-next.0 + - @backstage/plugin-todo-backend@0.3.11-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.3.10-next.0 + - example-app@0.2.93-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + ## 0.2.92 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index e2b8c5d50f..b66be07caf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.92", + "version": "0.2.93-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 5fb66ff4c0..f9606dc14d 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 1.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 1.6.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index f2be0fecb1..927286f36c 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.6.0", + "version": "1.6.1-next.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 01b34ddf2a..752e1ad5a0 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-model +## 1.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + ## 1.4.4 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index ac076ba8d2..232a7d2194 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "1.4.4", + "version": "1.4.5-next.0", "description": "Types and validators that help describe the model of a Backstage Catalog", "backstage": { "role": "common-library" diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index 571a1f412b..8aca4a4842 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/cli-node +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + ## 0.2.3 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index ee0e94215f..7e0f4d38b5 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.3", + "version": "0.2.4-next.0", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index b96f93db83..c6609b0249 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/cli +## 0.25.3-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- f86e34c: Removed unused `replace-in-file` dependency +- f4404e5: Add .ico import support +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/eslint-plugin@0.1.6-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.25.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 98f7b3bafb..f09bd7a516 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.25.2", + "version": "0.25.3-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index ea2fcc2f61..f8403f664c 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/config-loader +## 1.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + ## 1.6.2 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index fa549e039b..73ce9fc37c 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.6.2", + "version": "1.6.3-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 1ba8b45805..675687dffc 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/config +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + ## 1.1.1 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 97ca8c0af6..ddfe2da633 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config", - "version": "1.1.1", + "version": "1.1.2-next.0", "description": "Config API used by Backstage core, backend, and CLI", "backstage": { "role": "common-library" diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index ecc332d8eb..96d967f9a5 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-app-api +## 1.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 1.12.0 ### Minor Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 8a939fc5b6..60919dc9cd 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.12.0", + "version": "1.12.1-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 168e10d941..a21938e26f 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-compat-api +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/version-bridge@1.0.7 + ## 0.2.0 ### Minor Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index ee70bc8089..cfec392d1f 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.2.0", + "version": "0.2.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 78af1c4a5b..5deaf2dcf4 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/core-components +## 0.14.1-next.0 + +### Patch Changes + +- ff33ee2: Removed hardcoded font-family on select input +- ff7e126: Support i18n for core components +- 7854120: Create a component abstraction to consume system icons. +- ce73c3b: Removed the inline color from select icon to allow it to be colored via a theme +- a8f7904: `SignInPage`'s `'guest'` provider now supports the `@backstage/plugin-auth-backend-module-guest-provider` package to generate tokens. It will continue to use the old frontend-only auth as a fallback. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/version-bridge@1.0.7 + ## 0.14.0 ### Minor Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 04edbeb212..bdd9a1dc4d 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.14.0", + "version": "0.14.1-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index a66fb2eb83..ff2ab443b9 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-plugin-api +## 1.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 1.9.0 ### Minor Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 95dd1b40f4..1c79805470 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.9.0", + "version": "1.9.1-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index fe33cafe4c..4a701b3cd1 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.5.12-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.13 + ## 0.5.11 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 354f4d7cd9..94bd6c5a69 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.11", + "version": "0.5.12-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 7e7915a4bf..ae952e6924 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + ## 1.0.27 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 533ddc469a..2b72bb244d 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.27", + "version": "1.0.28-next.0", "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 998bc00a3f..9db25dbe50 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.12-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-common@0.1.13 + ## 0.2.12 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 40d66d7f4b..e4cfce13bb 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.12", + "version": "0.2.13-next.0", "private": true, "backstage": { "role": "cli" diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 92655a25d8..2a2f8c3461 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/errors +## 1.2.4-next.0 + +### Patch Changes + +- 2636075: Fixed an issue that was causing ResponseError not to report the HTTP status from the provided response. +- Updated dependencies + - @backstage/types@1.1.1 + ## 1.2.3 ### Patch Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index d62c5ea215..42c78287ab 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/errors", - "version": "1.2.3", + "version": "1.2.4-next.0", "description": "Common utilities for error handling within Backstage", "backstage": { "role": "common-library" diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 20bd1d900c..634f9579e9 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/eslint-plugin +## 0.1.6-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 + ## 0.1.5 ### Patch Changes diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ec3b9ceea8..63bec733be 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/eslint-plugin", "description": "Backstage ESLint plugin", - "version": "0.1.5", + "version": "0.1.6-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 38553d5fa2..63a9f1a223 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/frontend-app-api +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 0.6.0 ### Minor Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 40f6ce351f..87725b2211 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.6.0", + "version": "0.6.1-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index bd287c287b..c72758c50c 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/frontend-plugin-api +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 0.6.0 ### Minor Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 493d6fd553..0f2a08c48e 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.6.0", + "version": "0.6.1-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index fe590fd8e2..2433c64a6c 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/frontend-test-utils +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.6.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + ## 0.1.2 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 6c5ceb4d7c..4a22373e56 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.1.2", + "version": "0.1.3-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md index 17a0711779..1136932c75 100644 --- a/packages/integration-aws-node/CHANGELOG.md +++ b/packages/integration-aws-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-aws-node +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.9 ### Patch Changes diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index a515a01d31..9fb6e2f44b 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-aws-node", - "version": "0.1.9", + "version": "0.1.10-next.0", "description": "Helpers for fetching AWS account credentials", "backstage": { "role": "node-library" diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 8512b5b1a7..3160754124 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration-react +## 1.1.25-next.0 + +### Patch Changes + +- b38dc55: Updated `microsoftAuthApi` scopes for Azure DevOps to be fully qualified. +- Updated dependencies + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 1.1.24 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 6fad954df9..056a7cf25b 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.1.24", + "version": "1.1.25-next.0", "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 3cf0701f22..ac1e1ff19f 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration +## 1.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + ## 1.9.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 3b156ee323..b1932704ad 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.9.0", + "version": "1.9.1-next.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 3c4ba418b1..5787ba8a7e 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/repo-tools +## 0.6.3-next.0 + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/cli-node@0.2.4-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/cli-common@0.1.13 + ## 0.6.0 ### Minor Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index c14ca74fab..832dbc484e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.6.0", + "version": "0.6.3-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 58757665c8..f1cabfe53f 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.92-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/cli@0.25.3-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/app-defaults@1.5.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/core-app-api@1.12.1-next.0 + ## 0.2.91 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 1fcf83cc2f..5e8007a978 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.91", + "version": "0.2.92-next.0", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 0229a95f31..4564113b2c 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @techdocs/cli +## 1.8.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + ## 1.8.2 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index ad5982b093..ffb872856f 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.2", + "version": "1.8.5-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index c1db0fb567..2a6a294f2d 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/test-utils +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 1.5.0 ### Minor Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index c480df3673..f6ffcf7f72 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.5.0", + "version": "1.5.1-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index a0cf6e0a6c..2e6ad4b14d 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.5.2-next.0 + +### Patch Changes + +- 6f4d2a0: Exported `defaultTypography` to make adjusting these values in a custom theme easier + ## 0.5.1 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index ef3f56071f..80553c199f 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.5.1", + "version": "0.5.2-next.0", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index 4d6529e4b0..5cdd61452d 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-adr-backend +## 0.4.10-next.0 + +### Patch Changes + +- 334c5fe: Updated dependency `marked` to `^12.0.0`. +- c8fdd83: Migrated `DefaultAdrCollatorFactory` to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-adr-common@0.2.21-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 0216a8d4a1..02e4777db9 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.4.7", + "version": "0.4.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index aab9807c8d..3816462f69 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-adr-common +## 0.2.21-next.0 + +### Patch Changes + +- 5335634: Fixed Azure DevOps ADR file path reading +- Updated dependencies + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index d0cc173bbf..4fc8a5b70a 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-common", - "version": "0.2.20", + "version": "0.2.21-next.0", "description": "Common functionalities for the adr plugin", "backstage": { "role": "common-library" diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index 04612df31a..a5b171dedb 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-adr +## 0.6.14-next.0 + +### Patch Changes + +- 5335634: Fixed Azure DevOps ADR file path reading +- 669efc6: Remove unused package dependencies +- Updated dependencies + - @backstage/plugin-adr-common@0.2.21-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.6.13 ### Patch Changes diff --git a/plugins/adr/package.json b/plugins/adr/package.json index ea426bcd5f..efc2719e1e 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.6.13", + "version": "0.6.14-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index b029fbcd8e..33b13f5c6f 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-airbrake-backend +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.3.7 ### Patch Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 62b56edfb6..397f805884 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.3.7", + "version": "0.3.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index b46890f569..f9a7bb248a 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-airbrake +## 0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/dev-utils@1.0.28-next.0 + - @backstage/test-utils@1.5.1-next.0 + ## 0.3.30 ### Patch Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 8138bdc19f..83e81c2a83 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.30", + "version": "0.3.31-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 14d76ba90b..f3a2ce6e24 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-allure +## 0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.46 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 130daa540c..767f984144 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-allure", - "version": "0.1.46", + "version": "0.1.47-next.0", "description": "A Backstage plugin that integrates with Allure", "backstage": { "role": "frontend-plugin" diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index 1101cb71f7..f965d79f0c 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 5dec21a816..2b65513dda 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.2.0", + "version": "0.2.1-next.0", "backstage": { "role": "frontend-plugin-module" }, diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md index 13d7c9e020..0d44d35ddd 100644 --- a/plugins/analytics-module-ga4/CHANGELOG.md +++ b/plugins/analytics-module-ga4/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-ga4 +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json index 396ba80195..f182be09b1 100644 --- a/plugins/analytics-module-ga4/package.json +++ b/plugins/analytics-module-ga4/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga4", - "version": "0.2.0", + "version": "0.2.1-next.0", "backstage": { "role": "frontend-plugin-module" }, diff --git a/plugins/analytics-module-newrelic-browser/CHANGELOG.md b/plugins/analytics-module-newrelic-browser/CHANGELOG.md index 18b92da8b2..d4587faa0e 100644 --- a/plugins/analytics-module-newrelic-browser/CHANGELOG.md +++ b/plugins/analytics-module-newrelic-browser/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-analytics-module-newrelic-browser +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json index e443c5a56d..a704b9eaf0 100644 --- a/plugins/analytics-module-newrelic-browser/package.json +++ b/plugins/analytics-module-newrelic-browser/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-newrelic-browser", - "version": "0.1.0", + "version": "0.1.1-next.0", "backstage": { "role": "frontend-plugin-module" }, diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 32b7b76cc4..7752eb590a 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-apache-airflow +## 0.2.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 11317ce08f..8f68825418 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.20", + "version": "0.2.21-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index c4121dc841..dd06765163 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-api-docs +## 0.11.1-next.0 + +### Patch Changes + +- 7854120: Use the `AppIcon` component in the navigation item extension. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 0.11.0 ### Minor Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index fb42a30a27..7de2d4ef21 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.0", + "version": "0.11.1-next.0", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin" diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index f7ea472981..fd27695321 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-apollo-explorer +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index 2aac8cec9a..60323f87b2 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.20", + "version": "0.1.21-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index fc5f169d7c..d193fe3e9a 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-app-backend +## 0.3.61-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-app-node@0.1.13-next.0 + - @backstage/types@1.1.1 + ## 0.3.58 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 3a1093528e..1673f8d7b2 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.58", + "version": "0.3.61-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 10b8a721d8..64fc874317 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-node +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config-loader@1.6.3-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index e9e91d10a3..f509802cd6 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-node", "description": "Node.js library for the app plugin", - "version": "0.1.10", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index c2ec7ce563..99865e1aca 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.1.1 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index c49386e291..8180412063 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.1", + "version": "0.1.2-next.0", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin" diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 5325ba7a5e..99859e3299 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.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index ba73122270..7d4f129668 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", "description": "The atlassian-provider backend module for the auth plugin.", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index c24e78d04d..5cc5aeaa74 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.4-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index e79b1b8f61..907f0b574e 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", "description": "The aws-alb provider module for the Backstage auth backend.", - "version": "0.1.0", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 44cf3dedda..0a6b9372fb 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.8-next.0 + +### Patch Changes + +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/types@1.1.1 + ## 0.2.4 ### 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 54bd916f68..765ef6a11f 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", "description": "A GCP IAP auth provider module for the Backstage auth backend", - "version": "0.2.4", + "version": "0.2.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index c7dd3d160c..7dd171f7f5 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.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 1f1a02fd43..d684c55c06 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", "description": "The github-provider backend module for the auth plugin.", - "version": "0.1.7", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index 46b9887e8a..3f0136785c 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.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 2c55a8e6a2..32a5e20f25 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", "description": "The gitlab-provider backend module for the auth plugin.", - "version": "0.1.7", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index d3d3f923ab..478f12970c 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.10-next.0 + +### Patch Changes + +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 724081358a..3750cde69a 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", "description": "A Google auth provider module for the Backstage auth backend", - "version": "0.1.7", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md new file mode 100644 index 0000000000..b6b9e03bc3 --- /dev/null +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -0,0 +1,34 @@ +# @backstage/plugin-auth-backend-module-guest-provider + +## 0.1.0-next.0 + +### Minor Changes + +- 1bedb23: Adds a new guest provider that maps guest users to actual tokens. This also shifts the default guest login to `user:development/guest` to reduce overlap with your production/real data. To change that (or set it back to the old default, use the new `auth.providers.guest.userEntityRef` config key) like so, + + ```yaml title=app-config.yaml + auth: + providers: + guest: + userEntityRef: user:default/guest + ``` + + This also adds a new property to control the ownership entity refs, + + ```yaml title=app-config.yaml + auth: + providers: + guest: + ownershipEntityRefs: + - guests + - development/custom + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index bab85c2bd0..46ef0d96d7 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", "description": "The guest-provider backend module for the auth plugin.", - "version": "0.0.0", + "version": "0.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index ac4f11ec6a..993ddfbad5 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.8-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.5 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 028b77f88a..0a9a97cc9d 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", "description": "The microsoft-provider backend module for the auth plugin.", - "version": "0.1.5", + "version": "0.1.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 1f2f90949b..ad5fe30ef1 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.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 007c160c62..79efd86c23 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", "description": "The oauth2-provider backend module for the auth plugin.", - "version": "0.1.7", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index edd7670633..0a99aa33bf 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.6-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- e77d7a9: Internal refactor to avoid deprecated method. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.2 ### 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 26e2f0d4ff..f1ed2db494 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", "description": "The oauth2-proxy-provider backend module for the auth plugin.", - "version": "0.1.2", + "version": "0.1.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index b986cb78ca..bd43325f7b 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.1.3-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend@0.22.0-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 24cad9cec4..46eb7d1f99 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", "description": "The oidc-provider backend module for the auth plugin.", - "version": "0.1.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index c0b4ceca51..aeb7317627 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.0.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.0.3 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index dfcab1cdcb..8e765fc51c 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", "description": "The okta-provider backend module for the auth plugin.", - "version": "0.0.3", + "version": "0.0.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index be35f8d79e..68752b1842 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.7-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index 9581cb132b..fa35804afc 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", "description": "The pinniped-provider backend module for the auth plugin.", - "version": "0.1.4", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index 3bd1a3f2f7..1340932a92 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.1.5-next.0 + +### Patch Changes + +- 2af5354: Bump dependency `jose` to v5 +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 0.1.2 ### 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 841709b7ed..f4b66838ec 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.1.2", + "version": "0.1.5-next.0", "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 e2532247cc..c854cc958f 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-auth-backend +## 0.22.0-next.0 + +### Minor Changes + +- 293c835: Add support for Service Tokens to Cloudflare Access auth provider +- 492fe83: **BREAKING**: The `CatalogIdentityClient` constructor now also requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support the new auth services, which has also been done for the `createRouter` function. + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 2af5354: Bump dependency `jose` to v5 +- 38af71a: Updated dependency `google-auth-library` to `^9.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- fa7ea3f: Internal refactor to break out how the router is constructed +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.8-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.1.3-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.8-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.1.5-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.1.10-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.6-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.21.0 ### Minor Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 456cc18af2..244b507027 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.21.0", + "version": "0.22.0-next.0", "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 4f7b578234..72a978dc75 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-node +## 0.4.8-next.0 + +### Patch Changes + +- b4fc6e3: Deprecated the `getBearerTokenFromAuthorizationHeader` function, which is being replaced by the new `HttpAuthService`. +- 2af5354: Bump dependency `jose` to v5 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- b1b012d: Fix issue with `providerInfo` not being set properly for some proxy providers, by making `providerInfo` an explicit optional return from `authenticate` +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.4.4 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 7830126365..dc6ad90a9b 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.4.4", + "version": "0.4.8-next.0", "backstage": { "role": "node-library" }, diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index ca401bb067..68585d47fe 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-azure-devops-backend +## 0.6.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-azure-devops-common@0.4.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.5.2 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 2d5300a62a..9741177692 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.5.2", + "version": "0.6.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md index 1709b96073..d80c9d36ab 100644 --- a/plugins/azure-devops-common/CHANGELOG.md +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-devops-common +## 0.4.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + ## 0.3.2 ### Patch Changes diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 21e9245e38..23a575f95e 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-common", - "version": "0.3.2", + "version": "0.4.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 10d864ec6c..aae12231c2 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-azure-devops +## 0.4.0-next.0 + +### Minor Changes + +- 9fdb86a: Ability to fetch the README file from a different Azure DevOps path. + + Defaults to the current, Azure DevOps default behaviour (`README.md` in the root of the git repo); to use a different path, add the annotation `dev.azure.com/readme-path` + + Example: + + ```yaml + dev.azure.com/readme-path: /my-path/README.md + ``` + +- a9e7bd6: **BREAKING** The `AzureDevOpsClient` no longer requires `identityAPi` but now requires `fetchApi`. + + Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-azure-devops-common@0.4.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + ## 0.3.12 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 356d19cea0..12860d82cc 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.3.12", + "version": "0.4.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index 695de81aa0..dee6841c25 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-azure-sites-backend +## 0.3.0-next.0 + +### Minor Changes + +- 6b802a2: **BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- 85db926: Added new backend system for the Azure Sites backend plugin +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index 46cd4a5e79..ee03a221a1 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.2.0", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites-common/CHANGELOG.md b/plugins/azure-sites-common/CHANGELOG.md index 41194fb653..0561e060cb 100644 --- a/plugins/azure-sites-common/CHANGELOG.md +++ b/plugins/azure-sites-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-azure-sites-common +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/azure-sites-common/package.json b/plugins/azure-sites-common/package.json index 6b3ec694a1..a324d335a6 100644 --- a/plugins/azure-sites-common/package.json +++ b/plugins/azure-sites-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-common", - "version": "0.1.2", + "version": "0.1.3-next.0", "description": "Common functionalities for the azure plugin", "backstage": { "role": "common-library" diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 9761f8062c..5ee251a623 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-sites +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-azure-sites-common@0.1.3-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index f1a4e81e9b..5f99e77891 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.19", + "version": "0.1.20-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index fb05d58184..d9236a71ed 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-badges-backend +## 0.3.10-next.0 + +### Patch Changes + +- 29a1f91: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.3.7 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 9c87360a2a..4fd39c0356 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-badges-backend", - "version": "0.3.7", + "version": "0.3.10-next.0", "description": "A Backstage backend plugin that generates README badges for your entities", "backstage": { "role": "backend-plugin" diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 2d3f5ad46c..481f79ef2e 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-badges +## 0.2.55-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.54 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 0dc5fb81a5..e23f78911f 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-badges", - "version": "0.2.54", + "version": "0.2.55-next.0", "description": "A Backstage plugin that generates README badges for your entities", "backstage": { "role": "frontend-plugin" diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index e3622ba786..48ae09e437 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-bazaar-backend +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.3.8 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 0b286cfa5c..6e07bad83d 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.3.8", + "version": "0.3.11-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 56781a25b5..31eb5e1ac3 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-bazaar +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.22 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 2593e6f5f2..55f1bedb0f 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.22", + "version": "0.2.23-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index d8d2332cab..a86ea3a949 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.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.9.1-next.0 + ## 0.2.16 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 107da5b82e..8f16e59aed 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.2.16", + "version": "0.2.17-next.0", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library" diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 4c896b6daf..8a3d22667f 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-bitrise +## 0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.57 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index d25cdfbb8b..c75be5e4bf 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitrise", - "version": "0.1.57", + "version": "0.1.58-next.0", "description": "A Backstage plugin that integrates towards Bitrise", "backstage": { "role": "frontend-plugin" diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 0b43f3faee..13082e37c6 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.7-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 19e4571d96..10f1621ebc 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.3.4", + "version": "0.3.7-next.0", "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 3a0264280d..9502769253 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.32-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.29 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 2d92d7284f..ba645a0414 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.1.29", + "version": "0.1.32-next.0", "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 bb149cb885..c16a3b5564 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.1.6-next.0 + +### Patch Changes + +- 43a9ae1: Migrated to use new auth service. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index 3ce3ebd404..dde4e4f591 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.1.3", + "version": "0.1.6-next.0", "backstage": { "role": "backend-plugin-module" }, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 2f7ee2408e..1a87a2393b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.0-next.0 + +### Minor Changes + +- 9e527c9: BREAKING CHANGE: Migrates the `BitbucketCloudEntityProvider` to use the `EventsService`; fix new backend system support. + + `BitbucketCloudEntityProvider.fromConfig` accepts `events: EventsService` as optional argument to its `options`. + With provided `events`, the event-based updates/refresh will be available. + However, the `EventSubscriber` interface was removed including its `supportsEventTopics()` and `onEvent(params)`. + + The event subscription happens on `connect(connection)` if the `events` is available. + + **Migration:** + + ```diff + const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( + env.config, + { + catalogApi: new CatalogClient({ discoveryApi: env.discovery }), + + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + tokenManager: env.tokenManager, + }, + ); + - env.eventBroker.subscribe(bitbucketCloudProvider); + ``` + + **New Backend System:** + + Before this change, using this module with the new backend system was broken. + Now, you can add the catalog module for Bitbucket Cloud incl. event support backend. + Event support will always be enabled. + However, no updates/refresh will happen without receiving events. + + ```ts + backend.add( + import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'), + ); + ``` + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.17-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.25 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 441781331e..3bf4e8f8dd 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.25", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index be62f12f96..bff0ca6a4c 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.26-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.23 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 795652603d..36f5033e7f 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.1.23", + "version": "0.1.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index c8f278f494..30f53ce23d 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 64404eff32..cddb339d22 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.1.10", + "version": "0.1.13-next.0", "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 4fee0317d0..b21ef08d98 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.29-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.26 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index b0c0ff81ea..9d00315717 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.1.26", + "version": "0.1.29-next.0", "backstage": { "role": "backend-plugin-module" }, diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index e4a4fa5902..89ed3e781c 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-catalog-backend-module-github@0.5.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 8f2b683e61..a73e1c83e6 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.1.4", + "version": "0.1.7-next.0", "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 2d6f30a5a9..9aebb866c2 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-github +## 0.5.3-next.0 + +### Patch Changes + +- a936a8f: Migrated the `GithubLocationAnalyzer` to support new auth services. +- 999224f: Bump dependency `minimatch` to v9 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.5.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 6f3b933aa8..584b74502b 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.5.0", + "version": "0.5.3-next.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/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index ea85f7cef9..12a755d927 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.10-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.3.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 76a20bd97f..dda161f657 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.3.7", + "version": "0.3.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index a3e325c25f..f8cb905df0 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.17-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.4.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 02963f6bc1..b3c7df02b4 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.4.14", + "version": "0.4.17-next.0", "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 2fc6f1f379..49444bdaa0 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.28-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.5.25 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 7720e97aa6..54ecd7b737 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.25", + "version": "0.5.28-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 3f0cc568a4..9473525ad1 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.20-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.5.17 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index b2c09bf201..afa74aba92 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.5.17", + "version": "0.5.20-next.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 3d44a724da..ca587bec3f 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-backend@1.18.0-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.27 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index dd7cdd487b..192a115b89 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.1.27", + "version": "0.1.30-next.0", "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 add24532e0..617c243d53 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.18-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.1.15 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index c971e58299..9be14331e4 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.1.15", + "version": "0.1.18-next.0", "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 95689b70b4..7a7cfe753a 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 0.1.7 ### 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 d3df44cea7..faeef643df 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.1.7", + "version": "0.1.10-next.0", "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 73e67e9a96..cc16e4b51e 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.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 0.3.7 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index d0b306fecc..f3126af9f8 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", "description": "Backstage Catalog module to view unprocessed entities", - "version": "0.3.7", + "version": "0.3.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 3685a74c58..f1b09eb54c 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-catalog-backend +## 1.18.0-next.0 + +### Minor Changes + +- df12231: Allow setting EntityDataParser using CatalogModelExtensionPoint +- 15ba00f: Migrated to support new auth services. The `CatalogBuilder.create` method now accepts a `discovery` option, which is recommended to forward from the plugin environment, as it will otherwise fall back to use the `HostDiscovery` implementation. + +### Patch Changes + +- 999224f: Bump dependency `minimatch` to v9 +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 280edeb: Add index for original value in search table for faster entity facet response +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/repo-tools@0.6.3-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 1.17.0 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 7253f2db31..eff1c5838e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.17.0", + "version": "1.18.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 2dc6c4b356..bdce3bb20e 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-common +## 1.0.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 1.0.21 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 35858ee400..36283ba8b5 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.0.21", + "version": "1.0.22-next.0", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library" diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 8349ebcbff..13261916a9 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graph +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + ## 0.4.0 ### Minor Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 0aa451d86a..9f32c6c3ca 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.0", + "version": "0.4.1-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 3c17240a00..21e5249850 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-import +## 0.10.7-next.0 + +### Patch Changes + +- 75f686b: Fixed an issue generating a wrong entity link at the end of the import process +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.10.6 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 030a1ea036..3ae8336e07 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.10.6", + "version": "0.10.7-next.0", "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 76697f2b61..a7f1e6720b 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-node +## 1.8.0-next.0 + +### Minor Changes + +- df12231: Allow setting EntityDataParser using CatalogModelExtensionPoint + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 1.7.0 ### Minor Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 14d805b4d1..da50834dcc 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.7.0", + "version": "1.8.0-next.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 52074a5503..d98b9dcf9c 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-react +## 1.10.1-next.0 + +### Patch Changes + +- 930b5c1: Added 'root' and 'label' class keys for EntityAutocompletePicker, EntityOwnerPicker and EntityProcessingStatusPicker +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 1.10.0 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index ff89215fd7..407021ea0c 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.10.0", + "version": "1.10.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index be33d71aa3..d1a60a2b3c 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.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 4279cf4dcc..99c994a6eb 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.1.8", + "version": "0.1.9-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 54ce9bb816..205c7a7cff 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-catalog +## 1.17.1-next.0 + +### Patch Changes + +- 9332425: The entity page extension provided by the `/alpha` plugin now correctly renders the entity 404 page. +- 6727665: Allow the `spec.target` field to be searchable in the catalog table for locations. Previously, only the `spec.targets` field was be searchable. This makes locations generated by providers such as the `GithubEntityProvider` searchable in the catalog table. [#23098](https://github.com/backstage/backstage/issues/23098) +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 1.17.0 ### Minor Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e87b325991..d98cf25223 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.17.0", + "version": "1.17.1-next.0", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin" diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md index 75ed91f7a0..e06c671b69 100644 --- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cicd-statistics-module-gitlab +## 0.1.27-next.0 + +### Patch Changes + +- 402d991: Align `p-limit` dependency version to v3 +- Updated dependencies + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-cicd-statistics@0.1.33-next.0 + ## 0.1.26 ### Patch Changes diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index a08a2cb07c..48a0f1e3c4 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.1.26", + "version": "0.1.27-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 93cce5a7fe..f737bdaded 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cicd-statistics +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.32 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index d0a90efcc9..cd800ada52 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cicd-statistics", - "version": "0.1.32", + "version": "0.1.33-next.0", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", "backstage": { "role": "frontend-plugin" diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 3e4f5ad208..41d4d7e944 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.30 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index f60a3bb2bc..9f24bd7851 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.3.30", + "version": "0.3.31-next.0", "description": "A Backstage plugin that integrates towards Circle CI", "backstage": { "role": "frontend-plugin" diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index e744db0a91..71117b654c 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cloudbuild +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 0aed36d3cc..066d651338 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.4.0", + "version": "0.4.1-next.0", "description": "A Backstage plugin that integrates towards Google Cloud Build", "backstage": { "role": "frontend-plugin" diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 76c0241349..7ddd662395 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-code-climate +## 0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.30 ### Patch Changes diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 137c8729a4..ce31bea3ee 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.30", + "version": "0.1.31-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index f582f5a30e..98bf06357b 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-code-coverage-backend +## 0.2.27-next.0 + +### Patch Changes + +- cceebae: Fix jacoco convertor to not require annotation to be set to scm-only. +- 8efe690: Migrated to support new auth services. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.24 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index b44f608bbb..a0142c3265 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.2.24", + "version": "0.2.27-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index c672a138f2..2824a72abc 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-code-coverage +## 0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.23 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index bdefece916..111a01f196 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.23", + "version": "0.2.24-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index 3a5f884af0..9a14ab4e82 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-codescene +## 0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.22 ### Patch Changes diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index cf18ebdaf5..fcb1ddecc3 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.22", + "version": "0.1.23-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 216736ce13..23b5672979 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-config-schema +## 0.1.51-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.1.50 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index ba1c16af37..6ef240186d 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.50", + "version": "0.1.51-next.0", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin" diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index d9d741db61..9e4c859cef 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-cost-insights +## 0.12.20-next.0 + +### Patch Changes + +- 1b4fd09: Updated dependency `yup` to `^1.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-cost-insights-common@0.1.2 + ## 0.12.19 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 50c6119655..1e6638d792 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.12.19", + "version": "0.12.20-next.0", "description": "A Backstage plugin that helps you keep track of your cloud spend", "backstage": { "role": "frontend-plugin" diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 7f076b0c9f..8b611fc7e9 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-devtools-backend +## 0.3.0-next.0 + +### Minor Changes + +- 4dc5b48: **BREAKING**: The `createRouter` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config-loader@1.6.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.9-next.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index da4ce1ca28..15d2f0d8a2 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.2.7", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 3bbc48a585..d5036b1086 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-common +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/types@1.1.1 + ## 0.1.8 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index 8f127fa063..af0aa6aef7 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.8", + "version": "0.1.9-next.0", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library" diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 31ce5e2065..9efec5be1e 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-devtools +## 0.1.10-next.0 + +### Patch Changes + +- a0e3393: Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-devtools-common@0.1.9-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + ## 0.1.9 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 987c01cf2c..cc80401309 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.9", + "version": "0.1.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index d3d3ce0540..a59ad0f76c 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-dynatrace +## 9.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 9.0.0 ### Patch Changes diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index cc318ece25..442248ab48 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "9.0.0", + "version": "9.0.1-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/entity-feedback-backend/CHANGELOG.md b/plugins/entity-feedback-backend/CHANGELOG.md index 7e74da5f07..803ba234af 100644 --- a/plugins/entity-feedback-backend/CHANGELOG.md +++ b/plugins/entity-feedback-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-entity-feedback-backend +## 0.2.10-next.0 + +### Patch Changes + +- 4f8ecd6: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-entity-feedback-common@0.1.3 + ## 0.2.7 ### Patch Changes diff --git a/plugins/entity-feedback-backend/package.json b/plugins/entity-feedback-backend/package.json index 896b2c9c14..8906b52aae 100644 --- a/plugins/entity-feedback-backend/package.json +++ b/plugins/entity-feedback-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-feedback-backend", - "version": "0.2.7", + "version": "0.2.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md index b65968d94a..17a3827cbf 100644 --- a/plugins/entity-feedback/CHANGELOG.md +++ b/plugins/entity-feedback/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-entity-feedback +## 0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-entity-feedback-common@0.1.3 + ## 0.2.13 ### Patch Changes diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json index 3f888ad1b2..9bd8483600 100644 --- a/plugins/entity-feedback/package.json +++ b/plugins/entity-feedback/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-feedback", - "version": "0.2.13", + "version": "0.2.14-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md index 4ccd539432..c3dffced2a 100644 --- a/plugins/entity-validation/CHANGELOG.md +++ b/plugins/entity-validation/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-entity-validation +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.15 ### Patch Changes diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json index 7ae8a6404e..2c3e1a84d1 100644 --- a/plugins/entity-validation/package.json +++ b/plugins/entity-validation/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-entity-validation", - "version": "0.1.15", + "version": "0.1.16-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 679c3aeae6..4090745caf 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.0-next.0 + +### Minor Changes + +- 132d672: BREAKING CHANGE: Migrate `AwsSqsConsumingEventPublisher` and its backend module to use `EventsService`. + + Uses the `EventsService` instead of `EventBroker` at `AwsSqsConsumingEventPublisher`, + dropping the use of `EventPublisher` including `setEventBroker(..)`. + + Now, `AwsSqsConsumingEventPublisher.fromConfig` requires `events: EventsService` as option. + + ```diff + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: env.config, + + events: env.events, + logger: env.logger, + scheduler: env.scheduler, + }); + + await Promise.all(sqs.map(publisher => publisher.start())); + + // e.g. at packages/backend/src/plugins/events.ts + - await new EventsBackend(env.logger) + - .setEventBroker(env.eventBroker) + - .addPublishers(sqs) + - .start(); + + // or for other kinds of setups + - await Promise.all(sqs.map(publisher => publisher.setEventBroker(eventBroker))); + ``` + + `eventsModuleAwsSqsConsumingEventPublisher` uses the `eventsServiceRef` as dependency, + instead of `eventsExtensionPoint`. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.2.13 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 41f91d804e..23fbb7e7f7 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.2.13", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 5b24f805e4..b4c7078b18 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 0ee9f2dbfc..abec6269f0 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.1.20", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 0ef47c8ce7..2bf9126fb8 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 5273652f44..b57af9a7ec 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.1.20", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index d177b5203d..49d4784451 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 436fbde891..49071816f7 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.1.20", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index ccdc490bb0..921a3cdaac 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-events-backend-module-github +## 0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 209167f64c..18f3cd274e 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.1.20", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index 4a91b368bf..96adc084a5 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 08abb12b8f..0ba3950dff 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.1.20", + "version": "0.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index f8a63937dc..a9da4560c9 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.23-next.0 + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 6479a3e86d..c0254c05b7 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.20", + "version": "0.1.23-next.0", "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 3bf273a16d..73a5eed772 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-events-backend +## 0.3.0-next.0 + +### Minor Changes + +- c4bd794: BREAKING CHANGE: Migrate `HttpPostIngressEventPublisher` and `eventsPlugin` to use `EventsService`. + + Uses the `EventsService` instead of `EventBroker` at `HttpPostIngressEventPublisher`, + dropping the use of `EventPublisher` including `setEventBroker(..)`. + + Now, `HttpPostIngressEventPublisher.fromConfig` requires `events: EventsService` as option. + + ```diff + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + + events: env.events, + logger: env.logger, + }); + http.bind(eventsRouter); + + // e.g. at packages/backend/src/plugins/events.ts + - await new EventsBackend(env.logger) + - .setEventBroker(env.eventBroker) + - .addPublishers(http) + - .start(); + + // or for other kinds of setups + - await Promise.all(http.map(publisher => publisher.setEventBroker(eventBroker))); + ``` + + `eventsPlugin` uses the `eventsServiceRef` as dependency. + Unsupported (and deprecated) extension point methods will throw an error to prevent unintended behavior. + + ```ts + import { eventsServiceRef } from '@backstage/plugin-events-node'; + ``` + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.2.19 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index 9365d8c7dc..86962bc9cd 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.2.19", + "version": "0.3.0-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index dfa8e3c28c..91625a2b11 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,116 @@ # @backstage/plugin-events-node +## 0.3.0-next.0 + +### Minor Changes + +- eff3ca9: BREAKING CHANGE: Migrate `EventRouter` implementations from `EventBroker` to `EventsService`. + + `EventRouter` uses the new `EventsService` instead of the `EventBroker` now, + causing a breaking change to its signature. + + All of its extensions and implementations got adjusted accordingly. + (`SubTopicEventRouter`, `AzureDevOpsEventRouter`, `BitbucketCloudEventRouter`, + `GerritEventRouter`, `GithubEventRouter`, `GitlabEventRouter`) + + Required adjustments were made to all backend modules for the new backend system, + now also making use of the `eventsServiceRef` instead of the `eventsExtensionPoint`. + + **Migration:** + + Example for implementations of `SubTopicEventRouter`: + + ```diff + import { + EventParams, + + EventsService, + SubTopicEventRouter, + } from '@backstage/plugin-events-node'; + + export class GithubEventRouter extends SubTopicEventRouter { + - constructor() { + - super('github'); + + constructor(options: { events: EventsService }) { + + super({ + + events: options.events, + + topic: 'github', + + }); + } + + + protected getSubscriberId(): string { + + return 'GithubEventRouter'; + + } + + + // ... + } + ``` + + Example for a direct extension of `EventRouter`: + + ```diff + class MyEventRouter extends EventRouter { + - constructor(/* ... */) { + + constructor(options: { + + events: EventsService; + + // ... + + }) { + - super(); + // ... + + super({ + + events: options.events, + + topics: topics, + + }); + } + + + + protected getSubscriberId(): string { + + return 'MyEventRouter'; + + } + - + - supportsEventTopics(): string[] { + - return this.topics; + - } + } + ``` + +### Patch Changes + +- 56969b6: Add new `EventsService` as well as `eventsServiceRef` for the new backend system. + + **Summary:** + + - new: + `EventsService`, `eventsServiceRef`, `TestEventsService` + - deprecated: + `EventBroker`, `EventPublisher`, `EventSubscriber`, `DefaultEventBroker`, `EventsBackend`, + most parts of `EventsExtensionPoint` (alpha), + `TestEventBroker`, `TestEventPublisher`, `TestEventSubscriber` + + Add the `eventsServiceRef` as dependency to your backend plugins + or backend plugin modules. + + **Details:** + + The previous implementation using the `EventsExtensionPoint` was added in the early stages + of the new backend system and does not respect the plugin isolation. + This made it not compatible anymore with the new backend system. + + Additionally, the previous interfaces had some room for simplification, + supporting less exposure of internal concerns as well. + + Hereby, this change adds a new `EventsService` interface as replacement for the now deprecated `EventBroker`. + The new interface does not require any `EventPublisher` or `EventSubscriber` interfaces anymore. + Instead, it is expected that the `EventsService` gets passed into publishers and subscribers, + and used internally. There is no need to expose anything of that at their own interfaces. + + Most parts of `EventsExtensionPoint` (alpha) are deprecated as well and were not usable + (by other plugins or their modules) anyway. + + The `DefaultEventBroker` implementation is deprecated and wraps the new `DefaultEventsService` implementation. + Optionally, an instance can be passed as argument to allow mixed setups to operate alongside. + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.2.19 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index de794dfa45..c4c52d80a7 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-node", "description": "The plugin-events-node module for @backstage/plugin-events-backend", - "version": "0.2.19", + "version": "0.3.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index cea1a2bc36..1f99edf3a8 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @internal/plugin-todo-list-backend +## 1.0.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 1.0.22 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index c6792746e9..5763f8646d 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.22", + "version": "1.0.23-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index 98d6bafa7f..996e07f24f 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + ## 1.0.17 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index ccc97efe20..781a5d9d58 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.17", + "version": "1.0.18-next.0", "backstage": { "role": "common-library" }, diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 603c62aa38..40ffc85978 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.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 1.0.22 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 6faf4195a8..c13b594d04 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.22", + "version": "1.0.23-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md index 01121c27b1..f479b92c11 100644 --- a/plugins/explore-backend/CHANGELOG.md +++ b/plugins/explore-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-explore-backend +## 0.0.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-explore-common@0.0.2 + ## 0.0.20 ### Patch Changes diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index 285af827a5..ece82cd070 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-backend", - "version": "0.0.20", + "version": "0.0.23-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 0b932fe2d1..8c8e062c7e 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-explore-react +## 0.0.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-explore-common@0.0.2 + ## 0.0.36 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 6cae83ed87..bf00ed9804 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-react", - "version": "0.0.36", + "version": "0.0.37-next.0", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", "backstage": { "role": "web-library" diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 5f33b90d50..c0f0b11390 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-explore +## 0.4.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/plugin-explore-common@0.0.2 + - @backstage/plugin-explore-react@0.0.37-next.0 + ## 0.4.16 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 177464db55..5118a9bfba 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.4.16", + "version": "0.4.17-next.0", "description": "A Backstage plugin for building an exploration page of your software ecosystem", "backstage": { "role": "frontend-plugin" diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 7802f4de15..c3719e0634 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-firehydrant +## 0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index f7eb4ae52e..c35e92bddf 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-firehydrant", - "version": "0.2.14", + "version": "0.2.15-next.0", "description": "A Backstage plugin that integrates towards FireHydrant", "backstage": { "role": "frontend-plugin" diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index e9ea93c948..4d49cfa496 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-fossa +## 0.2.63-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.62 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index e3e45688b4..95136e34d2 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-fossa", - "version": "0.2.62", + "version": "0.2.63-next.0", "description": "A Backstage plugin that integrates towards FOSSA", "backstage": { "role": "frontend-plugin" diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index bd78001e47..03d51ee394 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gcalendar +## 0.3.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.23 ### Patch Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index cf536e5c69..14dcc698f3 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.23", + "version": "0.3.24-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index bcc087cf0d..c05cd8e3fc 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gcp-projects +## 0.3.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.46 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 0d4d30dd4a..5d6e91b97e 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.3.46", + "version": "0.3.47-next.0", "description": "A Backstage plugin that helps you manage projects in GCP", "backstage": { "role": "frontend-plugin" diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 4af88a88f4..adfeba3715 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.3.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.3.42 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 5dc5315209..5178246a2b 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.42", + "version": "0.3.43-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 573eafffdc..3de69b6bae 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-actions +## 0.6.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.6.11 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index c805baa08a..292adc09af 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.6.11", + "version": "0.6.12-next.0", "description": "A Backstage plugin that integrates towards GitHub Actions", "backstage": { "role": "frontend-plugin" diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index abeb448e6a..9b69945a56 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-github-deployments +## 0.1.62-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.61 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 35adeb3328..48ca4f1a3e 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-deployments", - "version": "0.1.61", + "version": "0.1.62-next.0", "description": "A Backstage plugin that integrates towards GitHub Deployments", "backstage": { "role": "frontend-plugin" diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index 0fe3cefd17..f288d5048f 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-issues +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.19 ### Patch Changes diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 5f00caa0d6..1811e69834 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.19", + "version": "0.2.20-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index 6137cafba0..cf0609e98d 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.25-next.0 + +### Patch Changes + +- 3c2d7c0: The `CardHeader` component in the `github-pull-requests-board` plugin will show the status for the PR +- 402d991: Align `p-limit` dependency version to v3 +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index e76bbb7505..663f08fd14 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.24", + "version": "0.1.25-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 96d6df21dd..abc82ce135 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gitops-profiles +## 0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.45 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index d75a81be96..a6b098e1c2 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.3.45", + "version": "0.3.46-next.0", "description": "A Backstage plugin that helps you manage GitOps profiles", "backstage": { "role": "frontend-plugin" diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 3d333d3535..9d82d1e8a8 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-gocd +## 0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.36 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 42cb7bd1ee..fdc8c3a5ac 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gocd", - "version": "0.1.36", + "version": "0.1.37-next.0", "description": "A Backstage plugin that integrates towards GoCD", "backstage": { "role": "frontend-plugin" diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 68319e5107..95ccedb0d5 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-graphiql +## 0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + ## 0.3.3 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index c5bc76ce2a..c42f825c9a 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-graphiql", - "version": "0.3.3", + "version": "0.3.4-next.0", "description": "Backstage plugin for browsing GraphQL APIs", "backstage": { "role": "frontend-plugin" diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md index aacf47b7b0..30cc7a805a 100644 --- a/plugins/graphql-voyager/CHANGELOG.md +++ b/plugins/graphql-voyager/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-graphql-voyager +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json index 42fc0ee8bf..edba31d88c 100644 --- a/plugins/graphql-voyager/package.json +++ b/plugins/graphql-voyager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-voyager", "description": "Backstage plugin for GraphQL Voyager", - "version": "0.1.12", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index dfd7f2a2fa..42744fbfae 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home-react +## 0.1.9-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index c2ef4d5c22..952e917084 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.8", + "version": "0.1.9-next.0", "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 ea500a5131..8cc2a38edb 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-home +## 0.6.3-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + ## 0.6.2 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 86c8efe80a..3cc8aa77a6 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.6.2", + "version": "0.6.3-next.0", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin" diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 73e3332de2..d56468962c 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-ilert +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.19 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 09ee791a4c..9684ca0472 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-ilert", - "version": "0.2.19", + "version": "0.2.20-next.0", "description": "A Backstage plugin that integrates towards iLert", "backstage": { "role": "frontend-plugin" diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index facffbe68b..1cf6847e0e 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-jenkins-backend +## 0.4.0-next.0 + +### Minor Changes + +- 55191cc: **BREAKING**: Both `createRouter` and `DefaultJenkinsInfoProvider.fromConfig` now require the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + + The `JenkinsInfoProvider` interface has been updated to receive `credentials` of the type `BackstageCredentials` rather than a token. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-jenkins-common@0.1.25-next.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index eaf8218a01..d4ffee38c3 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.3.4", + "version": "0.4.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index ae4f8a3c09..74ce3e0f44 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-jenkins-common +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index e9f4295645..7293d16c96 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.24", + "version": "0.1.25-next.0", "backstage": { "role": "common-library" }, diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 91268405bb..9c16246e2c 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-jenkins +## 0.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-jenkins-common@0.1.25-next.0 + ## 0.9.5 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 570c49734c..8ae7bc05a3 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.9.5", + "version": "0.9.6-next.0", "description": "A Backstage plugin that integrates towards Jenkins", "backstage": { "role": "frontend-plugin" diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 3f1c86473f..43e85b4f5d 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kafka-backend +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.3.8 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 29d538b354..398510211a 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka-backend", - "version": "0.3.8", + "version": "0.3.11-next.0", "description": "A Backstage backend plugin that integrates towards Kafka", "backstage": { "role": "backend-plugin" diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 3eb846527b..354c82553d 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kafka +## 0.3.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.30 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 9df3023ab9..7280a2e958 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.3.30", + "version": "0.3.31-next.0", "description": "A Backstage plugin that integrates towards Kafka", "backstage": { "role": "frontend-plugin" diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 2c499be769..6fb10afeb5 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-kubernetes-backend +## 0.16.0-next.0 + +### Minor Changes + +- e1e540c: **BREAKING**: The `KubernetesBuilder.createBuilder` method now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + - @backstage/types@1.1.1 + ## 0.15.0 ### Minor Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index a0a094588f..0c69e3f5b1 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.15.0", + "version": "0.16.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 8e8550daaf..c188b114bc 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-react@0.3.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.0.6 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index aa30d6b340..47b3ca237c 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.6", + "version": "0.0.7-next.0", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin" diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 8ebb66f857..f7ba85cdaa 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-common +## 0.7.5-next.0 + +### Patch Changes + +- 4642cb7: Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + ## 0.7.4 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 1c9a314a3c..3014a6decc 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.7.4", + "version": "0.7.5-next.0", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library" diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 18c9ee67da..d58e60a1ae 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-node +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + ## 0.1.4 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 76d2692767..1284133a36 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.1.4", + "version": "0.1.7-next.0", "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 1ab052def7..6618ae55cb 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-kubernetes-react +## 0.3.1-next.0 + +### Patch Changes + +- 4642cb7: Add support to fetch data for Daemon Sets and display an accordion in the same way as with Deployments +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.3.0 ### Minor Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 2d0eee2430..e3d1e7e371 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-react", "description": "Web library for the kubernetes-react plugin", - "version": "0.3.0", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 109f81af1b..3040e82470 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes +## 0.11.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.5-next.0 + - @backstage/plugin-kubernetes-react@0.3.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.11.5 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 5921c192f9..9bd42eb1e7 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.11.5", + "version": "0.11.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md index 0fdb72ff7d..c896e4b626 100644 --- a/plugins/lighthouse-backend/CHANGELOG.md +++ b/plugins/lighthouse-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-lighthouse-backend +## 0.4.5-next.0 + +### Patch Changes + +- 9f9ba70: **BREAKING**: The `createScheduler` function now requires the `discovery` service to be forwarded from the plugin environment. This is part of the migration to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-lighthouse-common@0.1.5-next.0 + ## 0.4.2 ### Patch Changes diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json index 1f68cb3972..a1ddd1dd36 100644 --- a/plugins/lighthouse-backend/package.json +++ b/plugins/lighthouse-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse-backend", - "version": "0.4.2", + "version": "0.4.5-next.0", "description": "Backend functionalities for lighthouse", "backstage": { "role": "backend-plugin" diff --git a/plugins/lighthouse-common/CHANGELOG.md b/plugins/lighthouse-common/CHANGELOG.md index ded0273147..13e9db39c7 100644 --- a/plugins/lighthouse-common/CHANGELOG.md +++ b/plugins/lighthouse-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-lighthouse-common +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.2-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/lighthouse-common/package.json b/plugins/lighthouse-common/package.json index d47b7c7edf..bcff040b12 100644 --- a/plugins/lighthouse-common/package.json +++ b/plugins/lighthouse-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse-common", - "version": "0.1.4", + "version": "0.1.5-next.0", "description": "Common functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend plugin", "backstage": { "role": "common-library" diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 0e3d2d2990..f0aed9bada 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-lighthouse +## 0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-lighthouse-common@0.1.5-next.0 + ## 0.4.15 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index f10f9ab650..7dee5990e2 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.4.15", + "version": "0.4.16-next.0", "description": "A Backstage plugin that integrates towards Lighthouse", "backstage": { "role": "frontend-plugin" diff --git a/plugins/linguist-backend/CHANGELOG.md b/plugins/linguist-backend/CHANGELOG.md index 54c69c8c57..e2cb827c5a 100644 --- a/plugins/linguist-backend/CHANGELOG.md +++ b/plugins/linguist-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-linguist-backend +## 0.5.10-next.0 + +### Patch Changes + +- 61ff58f: Migrated to support new auth services. +- 786c9c4: Updated dependency `luxon` to `^3.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-linguist-common@0.1.2 + ## 0.5.7 ### Patch Changes diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 8b1d0104c1..c71447c229 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist-backend", - "version": "0.5.7", + "version": "0.5.10-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md index 2579aa146f..97f3b00a06 100644 --- a/plugins/linguist/CHANGELOG.md +++ b/plugins/linguist/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-linguist +## 0.1.16-next.0 + +### Patch Changes + +- 4fb9600: Get component's title from translation file. See: https://backstage.io/docs/plugins/internationalization#for-an-application-developer-overwrite-plugin-messages +- a0e3393: Updated to use `fetchApi` as per [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013) +- 786c9c4: Updated dependency `luxon` to `^3.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-linguist-common@0.1.2 + ## 0.1.15 ### Patch Changes diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 9d59cea976..dbcc353a74 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist", - "version": "0.1.15", + "version": "0.1.16-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md index 29bc0412fa..fcbb9c1a03 100644 --- a/plugins/microsoft-calendar/CHANGELOG.md +++ b/plugins/microsoft-calendar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-microsoft-calendar +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json index b7a8883f7d..fe9f229375 100644 --- a/plugins/microsoft-calendar/package.json +++ b/plugins/microsoft-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-microsoft-calendar", - "version": "0.1.12", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index b65c5ec74b..31847f4921 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-newrelic-dashboard +## 0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.5 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index fc6bc9c671..8741b0c757 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.3.5", + "version": "0.3.6-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index c746258889..8de22b4fbf 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-newrelic +## 0.3.46-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.3.45 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index ba348f7cde..9fa6340725 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.3.45", + "version": "0.3.46-next.0", "description": "A Backstage plugin that integrates towards New Relic", "backstage": { "role": "frontend-plugin" diff --git a/plugins/nomad-backend/CHANGELOG.md b/plugins/nomad-backend/CHANGELOG.md index e2bfa5aa36..2b89aa93c0 100644 --- a/plugins/nomad-backend/CHANGELOG.md +++ b/plugins/nomad-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-nomad-backend +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/nomad-backend/package.json b/plugins/nomad-backend/package.json index dca0c09f66..c94ce6f40a 100644 --- a/plugins/nomad-backend/package.json +++ b/plugins/nomad-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad-backend", - "version": "0.1.12", + "version": "0.1.15-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/nomad/CHANGELOG.md b/plugins/nomad/CHANGELOG.md index 854a7a56bc..add14e483f 100644 --- a/plugins/nomad/CHANGELOG.md +++ b/plugins/nomad/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-nomad +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/nomad/package.json b/plugins/nomad/package.json index cb490032da..91416c3ca1 100644 --- a/plugins/nomad/package.json +++ b/plugins/nomad/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad", - "version": "0.1.11", + "version": "0.1.12-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 68fbd47a63..b8d1422a5c 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-notifications-backend +## 0.1.0-next.0 + +### Minor Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + +### Patch Changes + +- 5d9c5ba: The Notifications can be newly filtered based on the Created Date. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 84af361: Migrated to using the new auth services. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-notifications-node@0.1.0-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.0.1 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 8c11253a01..1689e95df6 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.0.1", + "version": "0.1.0-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/notifications-common/CHANGELOG.md b/plugins/notifications-common/CHANGELOG.md index d327cbcc80..1e6849e4af 100644 --- a/plugins/notifications-common/CHANGELOG.md +++ b/plugins/notifications-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-notifications-common +## 0.0.2-next.0 + +### Patch Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + ## 0.0.1 ### Patch Changes diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index b3b15798ee..dcce213b2f 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-common", - "version": "0.0.1", + "version": "0.0.2-next.0", "description": "Common functionalities for the notifications plugin", "backstage": { "role": "common-library" diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index c13ca24733..42afe10206 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications-node +## 0.1.0-next.0 + +### Minor Changes + +- 84af361: Migrated to using the new auth services. + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + ## 0.0.1 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 9150ce1816..d818c3566d 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-notifications-node", "description": "Node.js library for the notifications plugin", - "version": "0.0.1", + "version": "0.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 0e2623ed60..71a8685b8a 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications +## 0.1.0-next.0 + +### Minor Changes + +- 758f2a4: The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager. + +### Patch Changes + +- 5d9c5ba: The Notifications can be newly filtered based on the Created Date. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-notifications-common@0.0.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.2-next.0 + ## 0.0.1 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 13a565113b..5ffed363e7 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.0.1", + "version": "0.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md index f63c0b037d..af8a40ac68 100644 --- a/plugins/octopus-deploy/CHANGELOG.md +++ b/plugins/octopus-deploy/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-octopus-deploy +## 0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.12 ### Patch Changes diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index c297f370eb..360af7064c 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-octopus-deploy", - "version": "0.2.12", + "version": "0.2.13-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/opencost/CHANGELOG.md b/plugins/opencost/CHANGELOG.md index 061801789d..8cd5ae4e5f 100644 --- a/plugins/opencost/CHANGELOG.md +++ b/plugins/opencost/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-opencost +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json index 37ab923ff9..fdeb006ab6 100644 --- a/plugins/opencost/package.json +++ b/plugins/opencost/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-opencost", - "version": "0.2.5", + "version": "0.2.6-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 7b4fcd13b3..e44dbb9645 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org-react +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 96f77a29b4..39aa5a0752 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.19", + "version": "0.1.20-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 24243d3ae5..d0b6795b4d 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-org +## 0.6.21-next.0 + +### Patch Changes + +- 526f00a: Document the new frontend system extensions for the org plugin. +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.6.20 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 8981f564d4..f8c87628c0 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.20", + "version": "0.6.21-next.0", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin" diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 5110d1ccb4..4036dea415 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-pagerduty +## 0.7.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.7.2 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 128394adfb..539c3aefc9 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.7.2", + "version": "0.7.3-next.0", "description": "This plugin has been deprecated, consider using [@pagerduty/backstage-plugin](https://github.com/pagerduty/backstage-plugin) instead.", "backstage": { "role": "frontend-plugin" diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index d53ee5b2f8..0250c9bac6 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-periskop-backend +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.2.8 ### Patch Changes diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 161ab1956c..376312252e 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.2.8", + "version": "0.2.11-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index 4ddd668bad..ee61a17b98 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-periskop +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.28 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 52d30cb07a..e1b6c4557b 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.28", + "version": "0.1.29-next.0", "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 91610899ae..77f9cbb245 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + ## 0.1.7 ### 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 291b267bcf..1841c61351 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.1.7", + "version": "0.1.10-next.0", "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 d4a5c88f2b..cbce5b6c61 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-permission-backend +## 0.5.36-next.0 + +### Patch Changes + +- 9802004: Migrated to use the new auth services introduced in [BEP-0003](https://github.com/backstage/backstage/blob/master/beps/0003-auth-architecture-evolution/README.md). + + The `createRouter` function now accepts `auth`, `httpAuth` and `userInfo` options. Theses are used internally to support the new backend system, and can be ignored. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.5.33 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 4131dc29b9..e34689f2bb 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.33", + "version": "0.5.36-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index 74bd6db59b..1a78b40263 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-permission-common +## 0.7.13-next.0 + +### Patch Changes + +- 0502d82: The `token` option of the `PermissionEvaluator` methods is now deprecated. The options that only apply to backend implementations have been moved to `PermissionsService` from `@backstage/backend-plugin-api` instead. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.7.12 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 204e146e56..0668ece19c 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.7.12", + "version": "0.7.13-next.0", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library" diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 03be74d5ad..173a3288e9 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-permission-node +## 0.7.24-next.0 + +### Patch Changes + +- 0502d82: The `ServerPermissionClient` has been migrated to implement the `PermissionsService` interface, now accepting the new `BackstageCredentials` object in addition to the `token` option, which is now deprecated. It now also optionally depends on the new `AuthService`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.7.21 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 1b217d281c..a28de04385 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.21", + "version": "0.7.24-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 8141a61edf..fcde875dc6 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-react +## 0.4.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.4.20 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index ba8eafe830..6f5db94d97 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.20", + "version": "0.4.21-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index c6f8dd1ab0..29864d0cc8 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-playlist-backend +## 0.3.17-next.0 + +### Patch Changes + +- 6813366: Migrated to support new auth services. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-playlist-common@0.1.15-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index e7889830e1..7ae9b8c0a7 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.3.14", + "version": "0.3.17-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/playlist-common/CHANGELOG.md b/plugins/playlist-common/CHANGELOG.md index 0b65ee551f..4e67cc4a64 100644 --- a/plugins/playlist-common/CHANGELOG.md +++ b/plugins/playlist-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-playlist-common +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/playlist-common/package.json b/plugins/playlist-common/package.json index 2fae0ecdf0..c3eefbbf0b 100644 --- a/plugins/playlist-common/package.json +++ b/plugins/playlist-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-common", - "version": "0.1.14", + "version": "0.1.15-next.0", "description": "Common functionalities for the playlist plugin", "backstage": { "role": "common-library" diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index f5d5cadb66..2b4a967700 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-playlist +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-playlist-common@0.1.15-next.0 + ## 0.2.4 ### Patch Changes diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 512cbc74ab..7cd423e800 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.2.4", + "version": "0.2.5-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 8189aa3ef8..2c59cefb39 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-proxy-backend +## 0.4.11-next.0 + +### Patch Changes + +- 1b4fd09: Updated dependency `yup` to `^1.0.0`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.4.8 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 5e4ed1e728..731d1882fb 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.4.8", + "version": "0.4.11-next.0", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin" diff --git a/plugins/puppetdb/CHANGELOG.md b/plugins/puppetdb/CHANGELOG.md index bae5542cc7..6ff954c2cc 100644 --- a/plugins/puppetdb/CHANGELOG.md +++ b/plugins/puppetdb/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-puppetdb +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index e3017182b4..7c3ff00dc3 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-puppetdb", - "version": "0.1.13", + "version": "0.1.14-next.0", "description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.", "backstage": { "role": "frontend-plugin" diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 40924ec9b0..686d37df31 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-rollbar-backend +## 0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.55 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index aea99d495a..222cb2c30e 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.55", + "version": "0.1.58-next.0", "description": "A Backstage backend plugin that integrates towards Rollbar", "backstage": { "role": "backend-plugin" diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 928bf3d278..a4f475b6e2 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-rollbar +## 0.4.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.4.30 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 6990f08141..76df170ce5 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.4.30", + "version": "0.4.31-next.0", "description": "A Backstage plugin that integrates towards Rollbar", "backstage": { "role": "frontend-plugin" diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index 994e54a6b1..229a04b961 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.5-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index b7902cc68a..a283e42c5e 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", "description": "The azure module for @backstage/plugin-scaffolder-backend", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 039d7d80a5..28ac34a103 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index b6aa2acca4..eab9df6925 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", - "version": "0.1.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 39293f6299..34e5732af3 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 1f3abc2a8e..c0626baffb 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", - "version": "0.1.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 658c5f587f..d418e9df6f 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index d05b85a9de..45c2a6436d 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", - "version": "0.2.0", + "version": "0.2.3-next.0", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 4f65016afb..69fdf9782e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.14-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.11 ### 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 d674cea8c4..63d9979f8d 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", - "version": "0.2.11", + "version": "0.2.14-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index e2535d5b9c..6df6d4e404 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.37-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.2.34 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 6d4223a565..d1b7fffc34 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.34", + "version": "0.2.37-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 8bfc5820ca..fa6a6641ff 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.5-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index be7abf15e1..643ab5d963 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", - "version": "0.1.2", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 30fb0e31b7..920087cdba 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 976f445ea5..055a02d348 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", "description": "The gitea module for @backstage/plugin-scaffolder-backend", - "version": "0.1.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 15e8f0adfd..818502d7cd 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.2.3-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- 1753898: Updated dependency `octokit-plugin-create-pull-request` to `^5.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 15e148b49c..a3b02ec189 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", "description": "The github module for @backstage/plugin-scaffolder-backend", - "version": "0.2.0", + "version": "0.2.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index 1b1c2de247..9f382185ac 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.2.16-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.2.13 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index f24a967637..b9157b4231 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.2.13", + "version": "0.2.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index ad651f0e3a..582aeb8be6 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.30-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.4.27 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 59950702c6..7e462fc4d4 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.27", + "version": "0.4.30-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 09aa5f087d..b7a3182d30 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.21-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.18 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 3b1cfcc276..0a4bec24b9 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.1.18", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 083a3d9149..880a83db86 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.34-next.0 + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.0-next.0 + - @backstage/types@1.1.1 + ## 0.2.31 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 6631d7833c..508bc83ac3 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.2.31", + "version": "0.2.34-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b5786cab14..02ab750500 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend +## 1.22.0-next.0 + +### Minor Changes + +- c6b132e: Introducing checkpoints for scaffolder task action idempotency + +### Patch Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.5-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.5-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.3-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.10-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 1.21.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 61bd5ef49b..80db4c1c75 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.21.0", + "version": "1.22.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 5fcfd731b6..1cbeccbd7e 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-common +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/types@1.1.1 + ## 1.5.0 ### Minor Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index a72ccfbb82..fe2a049db9 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.5.0", + "version": "1.5.1-next.0", "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 2943a2a755..ea11cd151f 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1 +1,18 @@ # @backstage/plugin-scaffolder-node-test-utils + +## 0.1.0-next.0 + +### Minor Changes + +- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. + + It will help to maintain tests in a long run during structural changes of action context. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.3.3-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-scaffolder-node@0.3.3-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index 8c30261707..19d665bc75 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.0.1", + "version": "0.1.0-next.0", "backstage": { "role": "node-library" }, diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 3a5c9cd8b6..950a3cd977 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-scaffolder-node +## 0.3.3-next.0 + +### Patch Changes + +- 85f4723: Fixed file corruption for non UTF-8 data in fetch contents +- c6b132e: Introducing checkpoints for scaffolder task action idempotency +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 6de7c48782..6bb3d96aa8 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.3.0", + "version": "0.3.3-next.0", "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 6d08ec19fe..fc7267dba7 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-scaffolder-react +## 1.8.1-next.0 + +### Patch Changes + +- 930b5c1: Added 'root' and 'label' class key to TemplateCategoryPicker +- 6d649d2: Updated dependency `flatted` to `3.3.1`. +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 1.8.0 ### Minor Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 1fa5b63d92..51203909a0 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.8.0", + "version": "1.8.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 529aacda8a..055516a22f 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-scaffolder +## 1.18.1-next.0 + +### Patch Changes + +- 0cecb09: Updated dependency `@rjsf/utils` to `5.17.1`. + Updated dependency `@rjsf/core` to `5.17.1`. + Updated dependency `@rjsf/material-ui` to `5.17.1`. + Updated dependency `@rjsf/validator-ajv8` to `5.17.1`. +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-scaffolder-react@1.8.1-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.22-next.0 + - @backstage/plugin-permission-react@0.4.21-next.0 + - @backstage/plugin-scaffolder-common@1.5.1-next.0 + ## 1.18.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 18d333aa39..1fe465a84e 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.18.0", + "version": "1.18.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index 32a58d44a5..c03af4c62f 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index ee14c15b1a..31eed8201d 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.1.14", + "version": "0.1.17-next.0", "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 a025cc9264..d27031156d 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.3.16-next.0 + +### Patch Changes + +- 744c0cb: Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + ## 1.3.13 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index f177121a2e..7d4b2b3edb 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.3.13", + "version": "1.3.16-next.0", "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 c84bd973f9..6e99f20032 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-explore-common@0.0.2 + ## 0.1.14 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 9a69c90259..6387b0d21b 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.1.14", + "version": "0.1.17-next.0", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index db27db8394..2f2d19cbff 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.22-next.0 + +### Patch Changes + +- 744c0cb: Start importing `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` from the `@backstage/plugin-search-backend-node`. +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.5.19 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 9344e1c5ba..8bd704e838 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.19", + "version": "0.5.22-next.0", "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 f8fa943043..1c09019adf 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.1.3 ### 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 4632ea13b8..3d1aa504f1 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.1.3", + "version": "0.1.6-next.0", "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 d0daa993c0..e713a166a2 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.17-next.0 + +### Patch Changes + +- bb368a5: Migrated to support new auth services. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index f4a17db6b1..22775f8f4a 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.1.14", + "version": "0.1.17-next.0", "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 6b372d5262..8ab6f671a3 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search-backend-node +## 1.2.17-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- 744c0cb: Exports `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` types. These new types were extracted from the `@backstage/plugin-search-common` package and the `token` property was deprecated in favor of the a new credentials one. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + ## 1.2.14 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 7ef7b7d2c5..8d60757b4e 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.2.14", + "version": "1.2.17-next.0", "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 f9a773139b..cd91d27b41 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend +## 1.5.3-next.0 + +### Patch Changes + +- 744c0cb: Update the router to use the new `auth` services, it now accepts an optional discovery service option to get credentials for the permission service. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.17-next.0 + - @backstage/plugin-permission-node@0.7.24-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 1.5.0 ### Minor Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 9e073d7834..ee7836218f 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.5.0", + "version": "1.5.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index d14a8e37b3..b435f1b361 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-common +## 1.2.11-next.0 + +### Patch Changes + +- 744c0cb: Deprecate `QueryTranslator`, `QueryRequestOptions` and `SearchEngine` in favor of the types exported from `@backstage/plugin-search-backend-node`. +- Updated dependencies + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/types@1.1.1 + ## 1.2.10 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index cdec0b98f4..09b3eb928c 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-common", - "version": "1.2.10", + "version": "1.2.11-next.0", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "backstage": { "role": "common-library" diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 055589e9d7..2c7fc67ddc 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-react +## 1.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 1.7.6 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 79223e13b1..747e0e1d6f 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.6", + "version": "1.7.7-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index fbae4f5f40..a1ff6bf496 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-search +## 1.4.7-next.0 + +### Patch Changes + +- f0464b0: Removes ADR from the default set of search filters +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.7 + ## 1.4.6 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 6b67fd95bf..46ae937b4f 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.6", + "version": "1.4.7-next.0", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin" diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index c9df5802da..1c996c9286 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.5.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.5.15 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index c28be2cb47..09aab2400c 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.5.15", + "version": "0.5.16-next.0", "description": "A Backstage plugin that integrates towards Sentry", "backstage": { "role": "frontend-plugin" diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index a6385d0575..18b6f13f30 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-shortcuts +## 0.3.20-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.3.19 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index ef8bde1043..64e1ee50bd 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-shortcuts", - "version": "0.3.19", + "version": "0.3.20-next.0", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", "backstage": { "role": "frontend-plugin" diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index a44910f2a5..21ede277ed 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-signals-backend +## 0.0.4-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-signals-node@0.0.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.0.1 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 4fcca7b0e6..4537229070 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.0.1", + "version": "0.0.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index f8c4e9d268..d20cb2dd95 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-signals-node +## 0.0.4-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/plugin-events-node@0.3.0-next.0 + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.0.1 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 03ef61739f..8ae5c4411c 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-signals-node", "description": "Node.js library for the signals plugin", - "version": "0.0.1", + "version": "0.0.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/signals-react/CHANGELOG.md b/plugins/signals-react/CHANGELOG.md index d05ab73868..bb88bccef3 100644 --- a/plugins/signals-react/CHANGELOG.md +++ b/plugins/signals-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-signals-react +## 0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + ## 0.0.1 ### Patch Changes diff --git a/plugins/signals-react/package.json b/plugins/signals-react/package.json index ebaecbf337..8c2aa15224 100644 --- a/plugins/signals-react/package.json +++ b/plugins/signals-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-react", - "version": "0.0.1", + "version": "0.0.2-next.0", "description": "Web library for the signals plugin", "backstage": { "role": "web-library" diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 758a607424..d96f7a94c8 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-signals +## 0.0.2-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- Updated dependencies + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.2-next.0 + ## 0.0.1 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 5f2f4222c2..2c3f9eb800 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.1", + "version": "0.0.2-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/sonarqube-backend/CHANGELOG.md b/plugins/sonarqube-backend/CHANGELOG.md index d43a52df74..8df87e8169 100644 --- a/plugins/sonarqube-backend/CHANGELOG.md +++ b/plugins/sonarqube-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sonarqube-backend +## 0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + ## 0.2.12 ### Patch Changes diff --git a/plugins/sonarqube-backend/package.json b/plugins/sonarqube-backend/package.json index 0b4c7fe86c..70f936c3ea 100644 --- a/plugins/sonarqube-backend/package.json +++ b/plugins/sonarqube-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-backend", - "version": "0.2.12", + "version": "0.2.15-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/sonarqube-react/CHANGELOG.md b/plugins/sonarqube-react/CHANGELOG.md index 9d4843c3ee..de35cce067 100644 --- a/plugins/sonarqube-react/CHANGELOG.md +++ b/plugins/sonarqube-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-sonarqube-react +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/sonarqube-react/package.json b/plugins/sonarqube-react/package.json index 83b1291edf..090d6b1cf4 100644 --- a/plugins/sonarqube-react/package.json +++ b/plugins/sonarqube-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-react", - "version": "0.1.13", + "version": "0.1.14-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 40fe22e151..faf78c194a 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sonarqube +## 0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-sonarqube-react@0.1.14-next.0 + ## 0.7.12 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 352254b0e5..7d25781c60 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.7.12", + "version": "0.7.13-next.0", "description": "", "backstage": { "role": "frontend-plugin" diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 596877448a..0c333fbf8e 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.4.19 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index d14316e21f..d9a10a44c4 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-splunk-on-call", - "version": "0.4.19", + "version": "0.4.20-next.0", "description": "A Backstage plugin that integrates towards Splunk On-Call", "backstage": { "role": "frontend-plugin" diff --git a/plugins/stack-overflow-backend/CHANGELOG.md b/plugins/stack-overflow-backend/CHANGELOG.md index 8bdcf5c7da..d33126467d 100644 --- a/plugins/stack-overflow-backend/CHANGELOG.md +++ b/plugins/stack-overflow-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-stack-overflow-backend +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.6-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index b47ef94c7e..02880d19c7 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", - "version": "0.2.14", + "version": "0.2.17-next.0", "description": "Deprecated, consider using @backstage/plugin-search-backend-module-stack-overflow-collator instead", "backstage": { "role": "backend-plugin" diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index dddf1b6cb1..bdd79e3a1c 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-stack-overflow +## 0.1.26-next.0 + +### Patch Changes + +- c6779ac: fix: fix decode issues in title and author fields in `StackOverflowSearchResultListItem` +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/plugin-home-react@0.1.9-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + ## 0.1.25 ### Patch Changes diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 4a25eec41a..a39dc50e21 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.25", + "version": "0.1.26-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/stackstorm/CHANGELOG.md b/plugins/stackstorm/CHANGELOG.md index c7054de9d1..a55c8606e2 100644 --- a/plugins/stackstorm/CHANGELOG.md +++ b/plugins/stackstorm/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-stackstorm +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/stackstorm/package.json b/plugins/stackstorm/package.json index ffec91508f..1ea9267bb2 100644 --- a/plugins/stackstorm/package.json +++ b/plugins/stackstorm/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stackstorm", - "version": "0.1.11", + "version": "0.1.12-next.0", "description": "A Backstage plugin that integrates towards StackStorm", "backstage": { "role": "frontend-plugin" diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 4f92285aa3..94c365c1d2 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + ## 0.1.42 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index d4dcb23f4b..0e8f69695b 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.42", + "version": "0.1.45-next.0", "backstage": { "role": "backend-plugin-module" }, diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index efba8e6de8..534b04e161 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-tech-insights-backend +## 0.5.27-next.0 + +### Patch Changes + +- 0fb419b: Updated dependency `uuid` to `^9.0.0`. + Updated dependency `@types/uuid` to `^9.0.0`. +- d621468: Added support for the new `AuthService`. +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/plugin-tech-insights-node@0.5.0-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + ## 0.5.24 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index b1d84acae1..d9463612b6 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.5.24", + "version": "0.5.27-next.0", "backstage": { "role": "backend-plugin" }, diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index 8cf6cf08c2..0775f196c5 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-tech-insights-node +## 0.5.0-next.0 + +### Minor Changes + +- d621468: **BREAKING**: The `FactRetrieverContext` type now contains an additional `auth` field. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + ## 0.4.16 ### Patch Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index b94be91a80..80ec88e839 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.4.16", + "version": "0.5.0-next.0", "backstage": { "role": "node-library" }, diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index 806d5cfc14..158f01e0b6 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-tech-insights +## 0.3.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-tech-insights-common@0.2.12 + ## 0.3.22 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index e2cf61f6b3..1706469c5d 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.3.22", + "version": "0.3.23-next.0", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 975ed82524..9e71c9cb4a 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-tech-radar +## 0.6.14-next.0 + +### Patch Changes + +- a2327ac: Fixed an issue with the "moved in direction" table header cell getting squished and becoming unreadable if a timeline description is too long +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + ## 0.6.13 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index dc329bf37e..48eee12ed3 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.6.13", + "version": "0.6.14-next.0", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", "backstage": { "role": "frontend-plugin" diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index a9aa835d6c..9bb4dd054d 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-catalog@1.17.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/plugin-techdocs@1.10.1-next.0 + - @backstage/test-utils@1.5.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/core-app-api@1.12.1-next.0 + ## 1.0.27 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 27d58a93ed..1c5ea7f7c3 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.27", + "version": "1.0.28-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index e9826e3efa..61339aa39d 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-techdocs-backend +## 1.9.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-techdocs-node@1.11.5-next.0 + - @backstage/plugin-permission-common@0.7.13-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.17-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-catalog-common@1.0.22-next.0 + ## 1.9.3 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 3543dcf5cb..5fc3408931 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "1.9.3", + "version": "1.9.6-next.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 f40a972fd5..3fb94974c4 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + ## 1.1.5 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 85e47fea06..3f29feabc9 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.5", + "version": "1.1.6-next.0", "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 f66baf3e8b..5509ecbddc 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-node +## 1.11.5-next.0 + +### Patch Changes + +- 5b4f565: Fix handling of default plugins that have configuration +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/integration-aws-node@0.1.10-next.0 + ## 1.11.2 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 083e595ec9..b368b2fd6d 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.11.2", + "version": "1.11.5-next.0", "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 8d8df2b8d5..f015597280 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-react +## 1.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/version-bridge@1.0.7 + ## 1.1.16 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index e1f50f629d..7e211405fb 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.1.16", + "version": "1.1.17-next.0", "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 884a639d0e..41fed29cd1 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-techdocs +## 1.10.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/integration-react@1.1.25-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/plugin-search-common@1.2.11-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/integration@1.9.1-next.0 + - @backstage/plugin-search-react@1.7.7-next.0 + - @backstage/plugin-techdocs-react@1.1.17-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + ## 1.10.0 ### Minor Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 47ff100ab0..e3abb21b03 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.0", + "version": "1.10.1-next.0", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin" diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 6c82800c63..d365b145a8 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-todo-backend +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/repo-tools@0.6.3-next.0 + - @backstage/plugin-catalog-node@1.8.0-next.0 + - @backstage/backend-openapi-utils@0.1.6-next.0 + - @backstage/catalog-client@1.6.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/integration@1.9.1-next.0 + ## 0.3.8 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 9d4dcc7c7e..7ddf484089 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.3.8", + "version": "0.3.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 85bdd7c27f..c083736e94 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-todo +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.34 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 0ec887afa1..39327382fb 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo", - "version": "0.2.34", + "version": "0.2.35-next.0", "description": "A Backstage plugin that lets you browse TODO comments in your source code", "backstage": { "role": "frontend-plugin" diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 83ef42c444..3002c9ca79 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-user-settings-backend +## 0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/plugin-auth-node@0.4.8-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/types@1.1.1 + ## 0.2.9 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 05da152f7f..27e9dde760 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.2.9", + "version": "0.2.12-next.0", "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 c78e407f4c..906764c842 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-user-settings +## 0.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/theme@0.5.2-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + - @backstage/frontend-plugin-api@0.6.1-next.0 + - @backstage/core-app-api@1.12.1-next.0 + - @backstage/core-compat-api@0.2.1-next.0 + - @backstage/types@1.1.1 + ## 0.8.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 2d5a8b118e..f515ddabe4 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.1", + "version": "0.8.2-next.0", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin" diff --git a/plugins/vault-backend/CHANGELOG.md b/plugins/vault-backend/CHANGELOG.md index 1666a64e99..7c9269d944 100644 --- a/plugins/vault-backend/CHANGELOG.md +++ b/plugins/vault-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-vault-backend +## 0.4.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.21.3-next.0 + - @backstage/errors@1.2.4-next.0 + - @backstage/backend-plugin-api@0.6.13-next.0 + - @backstage/backend-tasks@0.5.18-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/plugin-vault-node@0.1.6-next.0 + ## 0.4.3 ### Patch Changes diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 8f333003d4..655dbca05c 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault-backend", "description": "A Backstage backend plugin that integrates towards Vault", - "version": "0.4.3", + "version": "0.4.6-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/vault-node/CHANGELOG.md b/plugins/vault-node/CHANGELOG.md index 85a616ccaf..06ed6c89f3 100644 --- a/plugins/vault-node/CHANGELOG.md +++ b/plugins/vault-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-vault-node +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.13-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/vault-node/package.json b/plugins/vault-node/package.json index c4043d44d1..a205c17208 100644 --- a/plugins/vault-node/package.json +++ b/plugins/vault-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-vault-node", - "version": "0.1.3", + "version": "0.1.6-next.0", "description": "Node.js library for the vault plugin", "backstage": { "role": "node-library" diff --git a/plugins/vault/CHANGELOG.md b/plugins/vault/CHANGELOG.md index 33c127cf41..0a803b2f81 100644 --- a/plugins/vault/CHANGELOG.md +++ b/plugins/vault/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-vault +## 0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/plugin-catalog-react@1.10.1-next.0 + - @backstage/catalog-model@1.4.5-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.1.25 ### Patch Changes diff --git a/plugins/vault/package.json b/plugins/vault/package.json index d49330fe97..32ff0ea526 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-vault", - "version": "0.1.25", + "version": "0.1.26-next.0", "description": "A Backstage plugin that integrates towards Vault", "backstage": { "role": "frontend-plugin" diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index b1f71c6b31..38289e87df 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-xcmetrics +## 0.2.49-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/core-components@0.14.1-next.0 + - @backstage/core-plugin-api@1.9.1-next.0 + ## 0.2.48 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index e0063b8d05..dc0f00d581 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-xcmetrics", - "version": "0.2.48", + "version": "0.2.49-next.0", "description": "A Backstage plugin that shows XCode build metrics for your components", "backstage": { "role": "frontend-plugin" diff --git a/yarn.lock b/yarn.lock index 532c8a4332..1693b3c283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3499,6 +3499,18 @@ __metadata: languageName: unknown linkType: soft +"@backstage/catalog-client@npm:^1.6.0": + version: 1.6.0 + resolution: "@backstage/catalog-client@npm:1.6.0" + dependencies: + "@backstage/catalog-model": ^1.4.4 + "@backstage/errors": ^1.2.3 + cross-fetch: ^4.0.0 + uri-template: ^2.0.0 + checksum: f9e8117145a63e10c8a7643ebafa78b416724d495efa77ac5d9069f32bc9353b63602e6744e63329381ea3a6e24fea359c5b4e82b1a698cd7743d266049275d2 + languageName: node + linkType: hard + "@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" @@ -3512,7 +3524,19 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@npm:^1.4.3, @backstage/catalog-model@npm:^1.4.4": + version: 1.4.4 + resolution: "@backstage/catalog-model@npm:1.4.4" + dependencies: + "@backstage/errors": ^1.2.3 + "@backstage/types": ^1.1.1 + ajv: ^8.10.0 + lodash: ^4.17.21 + checksum: c04762fe638bd417dc0959a60d9e5bac47502046fed28ebba66e53e4da36a87ed9e90b4132d8a3d0f23a8e71bec4b1d8f4fdc3a32a7277ef5db2701e5f7a831a + languageName: node + linkType: hard + +"@backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -3760,7 +3784,18 @@ __metadata: languageName: unknown linkType: soft -"@backstage/config@^1.1.1, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": +"@backstage/config@npm:^1.1.1": + version: 1.1.1 + resolution: "@backstage/config@npm:1.1.1" + dependencies: + "@backstage/errors": ^1.2.3 + "@backstage/types": ^1.1.1 + lodash: ^4.17.21 + checksum: 60dec0799a97ef7d99dc43076862b7914bd4b0390d6de6300148cc635ab218c21a3df1bc4fe98f7a49a89de9950c1f562ae29ce9324f93acfd9bd31104e751ef + languageName: node + linkType: hard + +"@backstage/config@workspace:^, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" dependencies: @@ -3884,6 +3919,57 @@ __metadata: languageName: node linkType: hard +"@backstage/core-components@npm:^0.14.0": + version: 0.14.0 + resolution: "@backstage/core-components@npm:0.14.0" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.9.0 + "@backstage/errors": ^1.2.3 + "@backstage/theme": ^0.5.1 + "@backstage/version-bridge": ^1.0.7 + "@date-io/core": ^1.3.13 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + "@types/react-sparklines": ^1.7.0 + "@types/react-text-truncate": ^0.14.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.6.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-text-truncate: ^0.19.0 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: b6d48b71976361c13d8928e743699baad5788c619bd6e9ca536fbf8edafe1828b16002edc20207ddfce0858b760adf2748d4584beed7daa375afa8eefd827592 + languageName: node + linkType: hard + "@backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" @@ -3957,7 +4043,25 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-plugin-api@^1.8.0, @backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": +"@backstage/core-plugin-api@npm:^1.8.0, @backstage/core-plugin-api@npm:^1.8.2, @backstage/core-plugin-api@npm:^1.9.0": + version: 1.9.0 + resolution: "@backstage/core-plugin-api@npm:1.9.0" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/errors": ^1.2.3 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.7 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + history: ^5.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 843e1068baeb0b91f2355a5fd22a06a847313da266d1bd9c95b10ac17891672abc6a17993628206e1f2ae63d817934b8729c7d87eadb9efae7be8b0df5a015da + languageName: node + linkType: hard + +"@backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": version: 0.0.0-use.local resolution: "@backstage/core-plugin-api@workspace:packages/core-plugin-api" dependencies: @@ -4056,7 +4160,17 @@ __metadata: languageName: unknown linkType: soft -"@backstage/errors@^1.2.3, @backstage/errors@workspace:^, @backstage/errors@workspace:packages/errors": +"@backstage/errors@npm:^1.2.3": + version: 1.2.3 + resolution: "@backstage/errors@npm:1.2.3" + dependencies: + "@backstage/types": ^1.1.1 + serialize-error: ^8.0.1 + checksum: 00e367ed9c47404d391d3c4125f5e279fe99393734f86ec0b0102cbea2573c9e9a4a58ca6a09c159b4b543bb3adabe81554a5af6d4d12ee7f45c92ed404705d9 + languageName: node + linkType: hard + +"@backstage/errors@workspace:^, @backstage/errors@workspace:packages/errors": version: 0.0.0-use.local resolution: "@backstage/errors@workspace:packages/errors" dependencies: @@ -4104,6 +4218,26 @@ __metadata: languageName: unknown linkType: soft +"@backstage/frontend-plugin-api@npm:^0.6.0": + version: 0.6.0 + resolution: "@backstage/frontend-plugin-api@npm:0.6.0" + dependencies: + "@backstage/core-components": ^0.14.0 + "@backstage/core-plugin-api": ^1.9.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.7 + "@material-ui/core": ^4.12.4 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + lodash: ^4.17.21 + zod: ^3.22.4 + zod-to-json-schema: ^3.21.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: b63395c3cc5d3c2e5eec4acca95d6ae6edf22ecee0ef37a82b6ea6133b9f74a2dcf18e66a4b233cb65057ae764eb9464039b3cc09ec9035075850b3d9267fd6e + languageName: node + linkType: hard + "@backstage/frontend-plugin-api@workspace:^, @backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" @@ -4167,7 +4301,25 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration-react@^1.1.21, @backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": +"@backstage/integration-react@npm:^1.1.21, @backstage/integration-react@npm:^1.1.24": + version: 1.1.24 + resolution: "@backstage/integration-react@npm:1.1.24" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.9.0 + "@backstage/integration": ^1.9.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 4a1b35e5dc6707b637b980d332a0a21440e2de50694148439789feeaf891dedf3141b5da833a409a559efe85a7e9b9c77a02e567abc6d9e0740b81cead905c55 + languageName: node + linkType: hard + +"@backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": version: 0.0.0-use.local resolution: "@backstage/integration-react@workspace:packages/integration-react" dependencies: @@ -4191,6 +4343,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/integration@npm:^1.9.0": + version: 1.9.0 + resolution: "@backstage/integration@npm:1.9.0" + dependencies: + "@azure/identity": ^4.0.0 + "@backstage/config": ^1.1.1 + "@backstage/errors": ^1.2.3 + "@octokit/auth-app": ^4.0.0 + "@octokit/rest": ^19.0.3 + cross-fetch: ^4.0.0 + git-url-parse: ^14.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + checksum: 22427a0bee3e14c7caca7be6fe8daad99413ea798ae0aad349d9c96e0a3e1273f9fd42c89f776dde45287136b56c614c29bdd3d278e43bb1bc58a86c31c610c8 + languageName: node + linkType: hard + "@backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": version: 0.0.0-use.local resolution: "@backstage/integration@workspace:packages/integration" @@ -5672,7 +5841,18 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": +"@backstage/plugin-catalog-common@npm:^1.0.20, @backstage/plugin-catalog-common@npm:^1.0.21": + version: 1.0.21 + resolution: "@backstage/plugin-catalog-common@npm:1.0.21" + dependencies: + "@backstage/catalog-model": ^1.4.4 + "@backstage/plugin-permission-common": ^0.7.12 + "@backstage/plugin-search-common": ^1.2.10 + checksum: 06570e20dddaf80f61d49d55ce1aa4a8213969742975902dd21f34cc6e37b0b2bc6e4d05b373425a16329a26bf682fd675ad77dcc60966bac0f4fa1a29e5bb59 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-common@workspace:plugins/catalog-common" dependencies: @@ -5780,7 +5960,43 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@^1.9.1, @backstage/plugin-catalog-react@^1.9.3, @backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": +"@backstage/plugin-catalog-react@npm:^1.9.1, @backstage/plugin-catalog-react@npm:^1.9.3": + version: 1.10.0 + resolution: "@backstage/plugin-catalog-react@npm:1.10.0" + dependencies: + "@backstage/catalog-client": ^1.6.0 + "@backstage/catalog-model": ^1.4.4 + "@backstage/core-components": ^0.14.0 + "@backstage/core-plugin-api": ^1.9.0 + "@backstage/errors": ^1.2.3 + "@backstage/frontend-plugin-api": ^0.6.0 + "@backstage/integration-react": ^1.1.24 + "@backstage/plugin-catalog-common": ^1.0.21 + "@backstage/plugin-permission-common": ^0.7.12 + "@backstage/plugin-permission-react": ^0.4.20 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.7 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + classnames: ^2.2.6 + lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 + qs: ^6.9.4 + react-use: ^17.2.4 + yaml: ^2.0.0 + zen-observable: ^0.10.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: b1be2976d20c116fb94662bb944c6713246d944f0a2bfd0b911c58d76278f722b8bb50e400318e087c4f80b60b8fba95086521348828d464add298bdc8808751 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-react@workspace:plugins/catalog-react" dependencies: @@ -6974,7 +7190,25 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-home-react@^0.1.5, @backstage/plugin-home-react@workspace:^, @backstage/plugin-home-react@workspace:plugins/home-react": +"@backstage/plugin-home-react@npm:^0.1.5": + version: 0.1.8 + resolution: "@backstage/plugin-home-react@npm:0.1.8" + dependencies: + "@backstage/core-components": ^0.14.0 + "@backstage/core-plugin-api": ^1.9.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@rjsf/utils": 5.17.0 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 375a9523fb6fdb6f732c1b453a878f1e18582db4146daee0fe26a26579982a7955c16deefa3c65c11cf92c8c76aac8a5c1949dce10ac2ad16bde6abf6073f0d0 + languageName: node + linkType: hard + +"@backstage/plugin-home-react@workspace:^, @backstage/plugin-home-react@workspace:plugins/home-react": version: 0.0.0-use.local resolution: "@backstage/plugin-home-react@workspace:plugins/home-react" dependencies: @@ -7957,6 +8191,20 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-permission-common@npm:^0.7.12": + version: 0.7.12 + resolution: "@backstage/plugin-permission-common@npm:0.7.12" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/errors": ^1.2.3 + "@backstage/types": ^1.1.1 + cross-fetch: ^4.0.0 + uuid: ^8.0.0 + zod: ^3.22.4 + checksum: 0535539348e59dde0555c54722f1d6ae3f951f8b900c27b65cf94aa10f57a705b2dad756eca673edeebafaca139ff8b1e8b93a4ca8f371a33c9544b3b0fba744 + languageName: node + linkType: hard + "@backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-common@workspace:plugins/permission-common" @@ -7995,6 +8243,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-permission-react@npm:^0.4.20": + version: 0.4.20 + resolution: "@backstage/plugin-permission-react@npm:0.4.20" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.9.0 + "@backstage/plugin-permission-common": ^0.7.12 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + swr: ^2.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: f692173a8c5a3aa2351c8f84184bc7984a017791b1dd850333aac8b97b799dd9b0b7c37d36cdc4fa48ada615ecfa44b7150af74aecab33524481b47fe4e412c9 + languageName: node + linkType: hard + "@backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-react@workspace:plugins/permission-react" @@ -8881,6 +9146,16 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-search-common@npm:^1.2.10": + version: 1.2.10 + resolution: "@backstage/plugin-search-common@npm:1.2.10" + dependencies: + "@backstage/plugin-permission-common": ^0.7.12 + "@backstage/types": ^1.1.1 + checksum: e4faae5e46e34c352c6ecb7e981b9efb5c34c62263275cd32e035fe17a77c929e20200a90230e70958496a89b83139ac7168e476b8c1f6497052f1c48d3e1bdd + languageName: node + linkType: hard + "@backstage/plugin-search-common@workspace:^, @backstage/plugin-search-common@workspace:plugins/search-common": version: 0.0.0-use.local resolution: "@backstage/plugin-search-common@workspace:plugins/search-common" @@ -9925,7 +10200,39 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.5.0, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@npm:^0.4.4": + version: 0.4.4 + resolution: "@backstage/theme@npm:0.4.4" + dependencies: + "@emotion/react": ^11.10.5 + "@emotion/styled": ^11.10.5 + "@mui/material": ^5.12.2 + peerDependencies: + "@material-ui/core": ^4.12.2 + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: 562ce0f0fd07202b44971b55bba9c39cd13c91a65873034d2e68fb5833c0d9882c2fd967d6c779a8563618ce035ec159823c865b0163911b7004f1bfce3ce4a1 + languageName: node + linkType: hard + +"@backstage/theme@npm:^0.5.0, @backstage/theme@npm:^0.5.1": + version: 0.5.1 + resolution: "@backstage/theme@npm:0.5.1" + dependencies: + "@emotion/react": ^11.10.5 + "@emotion/styled": ^11.10.5 + "@mui/material": ^5.12.2 + peerDependencies: + "@material-ui/core": ^4.12.2 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: 0c4d6481d6648962c19f8e08206ac2d459edf2fed85371452a5164eaeff4d0322517c99639f745a13bac2f7a8290c1c94e6cde79a1d85300b6c9b4e6b149972c + languageName: node + linkType: hard + +"@backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -9945,22 +10252,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@npm:^0.4.4": - version: 0.4.4 - resolution: "@backstage/theme@npm:0.4.4" - dependencies: - "@emotion/react": ^11.10.5 - "@emotion/styled": ^11.10.5 - "@mui/material": ^5.12.2 - peerDependencies: - "@material-ui/core": ^4.12.2 - "@types/react": ^16.13.1 || ^17.0.0 - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - checksum: 562ce0f0fd07202b44971b55bba9c39cd13c91a65873034d2e68fb5833c0d9882c2fd967d6c779a8563618ce035ec159823c865b0163911b7004f1bfce3ce4a1 - languageName: node - linkType: hard - "@backstage/types@^1.1.1, @backstage/types@workspace:^, @backstage/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@backstage/types@workspace:packages/types" @@ -15514,6 +15805,21 @@ __metadata: languageName: node linkType: hard +"@rjsf/utils@npm:5.17.0": + version: 5.17.0 + resolution: "@rjsf/utils@npm:5.17.0" + dependencies: + json-schema-merge-allof: ^0.8.1 + jsonpointer: ^5.0.1 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + react-is: ^18.2.0 + peerDependencies: + react: ^16.14.0 || >=17 + checksum: 01d0001f83083764a8552e009aa7df084621df9d1fc6ccdfad9d534513084421b1ad7494cab77b9b8205d680fd915f612d87800e20ab242e7066f33184c73d4f + languageName: node + linkType: hard + "@rjsf/utils@npm:5.17.1": version: 5.17.1 resolution: "@rjsf/utils@npm:5.17.1" From b9ee7c30520466a460b3552d5e25e81668500fac Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Tue, 27 Feb 2024 13:58:44 -0600 Subject: [PATCH 436/483] Fixed `deploy_docker-image.yml` workflow syntax Signed-off-by: Andre Wanlin --- .github/workflows/deploy_docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index eef70ff02c..0c83a05371 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -61,7 +61,7 @@ jobs: with: context: './example-app' file: ./example-app/packages/backend/Dockerfile - push: ${{ (github.event_name == "repository_dispatch") && (github.event.action == "release-published") }} + push: ${{ (github.event_name == 'repository_dispatch') && (github.event.action == 'release-published') }} platforms: linux/amd64,linux/arm64 tags: | ghcr.io/${{ github.repository_owner }}/backstage:latest From e150fe7e19947f98d45b2e5c6c5ec983a51cfc0f Mon Sep 17 00:00:00 2001 From: nikolar Date: Tue, 27 Feb 2024 16:56:09 -0800 Subject: [PATCH 437/483] fix empty response Signed-off-by: nikolar --- .../components/FeedbackResponseTable/FeedbackResponseTable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx index 0c26d7a2ba..2a756ee105 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx @@ -80,7 +80,8 @@ export const FeedbackResponseTable = (props: FeedbackResponseTableProps) => { width: '35%', render: (response: ResponseRow) => ( <> - {response.response?.length && + {response?.response && + response.response.length > 0 && response.response ?.split(',') .map(res => )} From df45710b5e04c6a4ba3034558b983276149f4f76 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Wed, 28 Feb 2024 09:17:30 +0200 Subject: [PATCH 438/483] feat(signals): take new auth in use for signals - fixed authentication for signals - improved error logging Signed-off-by: Heikki Hellgren --- .changeset/tender-apes-doubt.md | 5 + plugins/signals-backend/api-report.md | 6 + plugins/signals-backend/src/plugin.ts | 6 +- .../src/service/SignalManager.test.ts | 18 +-- .../src/service/SignalManager.ts | 14 ++- .../src/service/router.test.ts | 6 + plugins/signals-backend/src/service/router.ts | 110 ++++++++++++------ .../src/service/standaloneServer.ts | 15 +++ 8 files changed, 124 insertions(+), 56 deletions(-) create mode 100644 .changeset/tender-apes-doubt.md diff --git a/.changeset/tender-apes-doubt.md b/.changeset/tender-apes-doubt.md new file mode 100644 index 0000000000..f727bdeb25 --- /dev/null +++ b/.changeset/tender-apes-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-signals-backend': patch +--- + +Improved error logging and fixed authentication diff --git a/plugins/signals-backend/api-report.md b/plugins/signals-backend/api-report.md index 8bdaa581f3..5724c14aff 100644 --- a/plugins/signals-backend/api-report.md +++ b/plugins/signals-backend/api-report.md @@ -3,18 +3,22 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { EventBroker } from '@backstage/plugin-events-node'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { UserInfoService } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export interface RouterOptions { + // (undocumented) + auth?: AuthService; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) @@ -23,6 +27,8 @@ export interface RouterOptions { identity: IdentityApi; // (undocumented) logger: LoggerService; + // (undocumented) + userInfo?: UserInfoService; } // @public diff --git a/plugins/signals-backend/src/plugin.ts b/plugins/signals-backend/src/plugin.ts index 11b63163d5..f80a8a2dbd 100644 --- a/plugins/signals-backend/src/plugin.ts +++ b/plugins/signals-backend/src/plugin.ts @@ -33,15 +33,19 @@ export const signalsPlugin = createBackendPlugin({ logger: coreServices.logger, identity: coreServices.identity, discovery: coreServices.discovery, + userInfo: coreServices.userInfo, + auth: coreServices.auth, // TODO: EventBroker. It is optional for now but it's actually required so waiting for the new backend system // for the events-backend for this to work. }, - async init({ httpRouter, logger, identity, discovery }) { + async init({ httpRouter, logger, identity, discovery, userInfo, auth }) { httpRouter.use( await createRouter({ logger, identity, discovery, + userInfo, + auth, }), ); }, diff --git a/plugins/signals-backend/src/service/SignalManager.test.ts b/plugins/signals-backend/src/service/SignalManager.test.ts index 5720d1ff52..cf19f36224 100644 --- a/plugins/signals-backend/src/service/SignalManager.test.ts +++ b/plugins/signals-backend/src/service/SignalManager.test.ts @@ -126,25 +126,15 @@ describe('SignalManager', () => { // Connection with identity and subscription const ws2 = new MockWebSocket(); manager.addConnection(ws2 as unknown as WebSocket, { - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/john.doe'], - userEntityRef: 'user:default/john.doe', - }, - expiresInSeconds: 3600, - token: '1234', + ownershipEntityRefs: ['user:default/john.doe'], + userEntityRef: 'user:default/john.doe', }); // Connection without subscription const ws3 = new MockWebSocket(); manager.addConnection(ws3 as unknown as WebSocket, { - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/john.doe'], - userEntityRef: 'user:default/john.doe', - }, - expiresInSeconds: 3600, - token: '1234', + ownershipEntityRefs: ['user:default/john.doe'], + userEntityRef: 'user:default/john.doe', }); ws1.trigger( diff --git a/plugins/signals-backend/src/service/SignalManager.ts b/plugins/signals-backend/src/service/SignalManager.ts index db374418e1..c31c7869bd 100644 --- a/plugins/signals-backend/src/service/SignalManager.ts +++ b/plugins/signals-backend/src/service/SignalManager.ts @@ -18,8 +18,10 @@ import { SignalPayload } from '@backstage/plugin-signals-node'; import { RawData, WebSocket } from 'ws'; import { v4 as uuid } from 'uuid'; import { JsonObject } from '@backstage/types'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + BackstageUserInfo, + LoggerService, +} from '@backstage/backend-plugin-api'; /** * @internal @@ -64,14 +66,14 @@ export class SignalManager { }); } - addConnection(ws: WebSocket, identity?: BackstageIdentityResponse) { + addConnection(ws: WebSocket, identity?: BackstageUserInfo) { const id = uuid(); const conn = { id, - user: identity?.identity.userEntityRef ?? 'user:default/guest', + user: identity?.userEntityRef ?? 'user:default/guest', ws, - ownershipEntityRefs: identity?.identity.ownershipEntityRefs ?? [ + ownershipEntityRefs: identity?.ownershipEntityRefs ?? [ 'user:default/guest', ], subscriptions: new Set(), @@ -80,7 +82,7 @@ export class SignalManager { this.connections.set(id, conn); ws.on('error', (err: Error) => { - this.logger.info( + this.logger.error( `Error occurred with connection ${id}: ${err}, closing connection`, ); ws.close(); diff --git a/plugins/signals-backend/src/service/router.test.ts b/plugins/signals-backend/src/service/router.test.ts index 807b45d53e..64367ee779 100644 --- a/plugins/signals-backend/src/service/router.test.ts +++ b/plugins/signals-backend/src/service/router.test.ts @@ -23,6 +23,7 @@ import request from 'supertest'; import { createRouter } from './router'; import { EventBroker } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; +import { UserInfoService } from '@backstage/backend-plugin-api'; const eventBrokerMock: jest.Mocked = { subscribe: jest.fn(), @@ -38,6 +39,10 @@ const discovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; +const userInfo: jest.Mocked = { + getUserInfo: jest.fn(), +}; + describe('createRouter', () => { let app: express.Express; @@ -47,6 +52,7 @@ describe('createRouter', () => { identity: identityApiMock, eventBroker: eventBrokerMock, discovery, + userInfo, }); app = express().use(router); }); diff --git a/plugins/signals-backend/src/service/router.ts b/plugins/signals-backend/src/service/router.ts index cc8fe9b851..5a6f4469b2 100644 --- a/plugins/signals-backend/src/service/router.ts +++ b/plugins/signals-backend/src/service/router.ts @@ -14,22 +14,25 @@ * limitations under the License. */ import { + createLegacyAuthAdapters, errorHandler, PluginEndpointDiscovery, } from '@backstage/backend-common'; import express, { NextFunction, Request, Response } from 'express'; import Router from 'express-promise-router'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + AuthService, + BackstageUserInfo, + LoggerService, + UserInfoService, +} from '@backstage/backend-plugin-api'; import * as https from 'https'; import http, { IncomingMessage } from 'http'; import { SignalManager } from './SignalManager'; -import { - BackstageIdentityResponse, - IdentityApi, - IdentityApiGetIdentityRequest, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { EventBroker } from '@backstage/plugin-events-node'; import { WebSocket, WebSocketServer } from 'ws'; +import { Duplex } from 'stream'; /** @public */ export interface RouterOptions { @@ -37,21 +40,85 @@ export interface RouterOptions { eventBroker?: EventBroker; identity: IdentityApi; discovery: PluginEndpointDiscovery; + auth?: AuthService; + userInfo?: UserInfoService; } /** @public */ export async function createRouter( options: RouterOptions, ): Promise { - const { logger, identity, discovery } = options; + const { logger, discovery } = options; + const { auth, userInfo } = createLegacyAuthAdapters(options); + const manager = SignalManager.create(options); let subscribedToUpgradeRequests = false; + let apiUrl: string | undefined = undefined; const webSocketServer = new WebSocketServer({ noServer: true, clientTracking: false, }); + webSocketServer.on('error', (error: Error) => { + logger.error('WebSocket server error', error); + }); + + webSocketServer.on('close', () => { + logger.info('WebSocket server closed'); + }); + + const handleUpgrade = async ( + request: Request, + socket: Duplex, + head: Buffer, + ) => { + if (!apiUrl) { + apiUrl = await discovery.getBaseUrl('signals'); + } + + if (!request.url || !apiUrl || !apiUrl.endsWith(request.url)) { + return; + } + + let userIdentity: BackstageUserInfo | undefined = undefined; + + // Authentication token is passed in Sec-WebSocket-Protocol header as there + // is no other way to pass the token with plain websockets + try { + const token = request.headers['sec-websocket-protocol']; + if (token) { + const credentials = await auth.authenticate(token); + if (auth.isPrincipal(credentials, 'user')) { + userIdentity = await userInfo.getUserInfo(credentials); + } + } + } catch (e) { + logger.error('Failed to authenticate WebSocket connection', e); + socket.write( + 'HTTP/1.1 401 Web Socket Protocol Handshake\r\n' + + 'Upgrade: WebSocket\r\n' + + 'Connection: Upgrade\r\n' + + '\r\n', + ); + socket.destroy(); + return; + } + + try { + webSocketServer.handleUpgrade( + request, + socket, + head, + (ws: WebSocket, __: IncomingMessage) => { + manager.addConnection(ws, userIdentity); + }, + ); + } catch (e) { + logger.error('Failed to handle WebSocket upgrade', e); + } + }; + const upgradeMiddleware = async ( req: Request, _: Response, @@ -70,34 +137,7 @@ export async function createRouter( } subscribedToUpgradeRequests = true; - const apiUrl = await discovery.getBaseUrl('signals'); - server.on('upgrade', async (request, socket, head) => { - if (!request.url || !apiUrl.endsWith(request.url)) { - return; - } - - let userIdentity: BackstageIdentityResponse | undefined = undefined; - - // Authentication token is passed in Sec-WebSocket-Protocol header as there - // is no other way to pass the token with plain websockets - const token = req.headers['sec-websocket-protocol']; - if (token) { - userIdentity = await identity.getIdentity({ - request: { - headers: { authorization: token }, - }, - } as IdentityApiGetIdentityRequest); - } - - webSocketServer.handleUpgrade( - request, - socket, - head, - (ws: WebSocket, __: IncomingMessage) => { - manager.addConnection(ws, userIdentity); - }, - ); - }); + server.on('upgrade', handleUpgrade); }; const router = Router(); diff --git a/plugins/signals-backend/src/service/standaloneServer.ts b/plugins/signals-backend/src/service/standaloneServer.ts index 00ef7b5c77..f88848fffc 100644 --- a/plugins/signals-backend/src/service/standaloneServer.ts +++ b/plugins/signals-backend/src/service/standaloneServer.ts @@ -28,6 +28,11 @@ import { EventParams, EventSubscriber, } from '@backstage/plugin-events-node'; +import { + BackstageCredentials, + BackstageUserInfo, + UserInfoService, +} from '@backstage/backend-plugin-api'; export interface ServerOptions { port: number; @@ -64,11 +69,21 @@ export async function startStandaloneServer( eventBroker, }); + const userInfo: UserInfoService = { + async getUserInfo(_: BackstageCredentials): Promise { + return { + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }; + }, + }; + const router = await createRouter({ logger, identity, eventBroker, discovery, + userInfo, }); let service = createServiceBuilder(module) From 4e389ad60de9ce8fb428059ec654f0fd4cc646da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 19 Jan 2024 18:02:53 +0100 Subject: [PATCH 439/483] docs/frontend-system: add app migration docs Signed-off-by: Patrik Oldsberg --- .../building-apps/08-migrating.md | 435 ++++++++++++++++++ microsite/sidebars.json | 5 + 2 files changed, 440 insertions(+) create mode 100644 docs/frontend-system/building-apps/08-migrating.md diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md new file mode 100644 index 0000000000..8efda0e211 --- /dev/null +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -0,0 +1,435 @@ +--- +id: migrating +title: Migrating Apps +sidebar_label: Migration Guide +# prettier-ignore +description: How to migrate existing apps to the new frontend system +--- + +## Overview + +This section describes how to migrate an existing Backstage app package to use the new frontend system. The app package is typically found at `packages/app` in your project and is responsible for wiring together the Backstage frontend application. + +## Switching out `createApp` + +The first step in migrating an app is to switch out the `createApp` function for the new one from `@backstage/frontend-api-app`: + +```tsx title="in packages/app/src/App.tsx" +// highlight-remove-next-line +import { createApp } from '@backstage/app-defaults'; +// highlight-add-next-line +import { createApp } from '@backstage/frontend-app-api'; +``` + +This immediate switch will lead to a lot of breakages that we need to fix. Let's start by addressing `app.createRoot(...)`, which no longer accepts any arguments. + +Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together to an extension tree in the new system. Much more responsibility has been shifted to plugins in the new system, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/01-index.md) section. + +Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once. + +The helper is called `convertLegacyApp` and is exported from the `@backstage/core-compat-api` package, which you will need to add as a dependency to your app package: + +```bash +yarn add --cwd packages/app @backstage/core-compat-api +``` + +Once installed, import `convertLegacyApp`. If your app currently looks like this: + +```tsx title="in packages/app/src/App.tsx" +const app = createApp({ + /* other options */ +}); + +export default app.createRoot( + <> + + + + {routes} + + , +); +``` + +Migrate it to the following: + +```tsx title="in packages/app/src/App.tsx" +const legacyFeatures = convertLegacyApp( + <> + + + + {routes} + + , +); + +const app = createApp({ + /* other options */ + features: [...legacyFeatures], +}); + +export default app.createRoot(); +``` + +We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyApp(...)`. We then pass the features returned by `convertLegacyApp` and forward them to the `features` option of the new `createApp`. + +There is one more details that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather and a component, so we need to update our app `index.tsx` as follows: + +```tsx title="in packages/app/src/index.tsx" +import '@backstage/cli/asset-types'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +// highlight-remove-next-line +import App from './App'; +// highlight-add-next-line +import app from './App'; + +// highlight-remove-next-line +ReactDOM.createRoot(document.getElementById('root')!).render(); +// highlight-add-next-line +ReactDOM.createRoot(document.getElementById('root')!).render(app); +``` + +At this point the contents of your app should be past the initial migration stage, and we can move on to migrating any remaining options that you may have passed to `createApp`. + +## Migrating `createApp` Options + +Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension creator](../architecture/03-extensions.md#extension-creators) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/05-extension-overrides.md#create-standalone-extensions) section for more information. + +For example, assuming you have a `lightTheme` extension that you want to add to your app, you can use the following: + +```ts +const app = createApp({ + features: [ + createExtensionOverrides({ + extensions: [lightTheme], + }), + ], +}); +``` + +### `apis` + +[Utility API](../utility-apis/01-index.md) factories are now installed as extensions instead. Pass the existing factory to `createApiExtension` and install it in the app. For more information, see the section on [configuring Utility APIs](../utility-apis/04-configuring.md). + +For example, the following API configuration: + +```ts +const app = createApp({ + apis: [ + createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), + ], +}); +``` + +Can be converted to the following extension: + +```ts +const scmIntegrationsApi = createApiExtension({ + factory: createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), +}); +``` + +### `icons` + +Icons are currently installed through the usual options to `createApp`, but will be switched to use extensions in the future. + +### `plugins` + +Plugins are now passed through the `features` options instead. + +### `featureFlags` + +Declaring features flags in the app is no longer supported, move these declarations to the appropriate plugins instead. + +### `components` + +Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./index.md#TODO) for more information. + +The `Router` component is now a built-in extension that you can override using `createRouterExtension`. + +The Sign-in page is now installed as an extension using the `createSignInPageExtension` instead. See the section on [configuring the sign-in page](./index.md#TODO) for more information. + +For example, the following sign-in page configuration: + +```tsx +const app = createApp({ + components: { + SignInPage: props => ( + + ), + }, +}); +``` + +Can be converted to the following extension: + +```tsx +const signInPage = createSignInPageExtension({ + loader: async () => props => + ( + + ), +}); +``` + +### `themes` + +Themes are now installed as extensions instead using `createThemeExtension`. See the section on [configuring themes](./index.md#TODO) for more information. + +For example, the following theme configuration: + +```tsx +const app = createApp({ + themes: [ + { + id: 'light', + title: 'Light', + variant: 'light', + Provider: ({ children }) => ( + + ), + }, + ], +``` + +Can be converted to the following extension: + +```tsx +const lightTheme = createThemeExtension({ + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + Provider: ({ children }) => ( + + ), +}); +``` + +### `configLoader` + +The config loader API has been slightly changed. Rather than returning a promise for an array of `AppConfig` objects, it should now return the `ConfigApi` directly. + +```ts +const app = createApp({ + async configLoader() { + const appConfigs = await loadAppConfigs(); + // highlight-remove-next-line + return appConfigs; + // highlight-add-next-line + return { config: ConfigReader.fromConfigs(appConfigs) }; + }, +}); +``` + +### `bindRoutes` + +Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/07-routes.md#binding-external-route-references) for more information. + +Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyApp`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system. + +For example, if both the `catalogPlugin` and `scaffolderPlugin` are legacy plugins, you can bind their routes like this: + +```ts +const app = createApp({ + features: convertLegacyApp(...), + bindRoutes({ bind }) { + bind(convertLegacyRouteRefs(catalogPlugin.createComponent), { + registerApi: convertLegacyRouteRef(scaffolderPlugin.routes.root), + }); + }, +}); +``` + +### `__experimentalTranslations` + +Translations are now installed as extensions instead using `createTranslationExtension`. See the section on [configuring translations](./index.md#TODO) for more information. + +## Gradual Migration + +After updating all `createApp` options as well as using `convertLegacyApp` to use our existing app structure, you should be able to start up the app and see that it still works. If that is not the case, make sure you read any errors messages that you may see in the app as they can provide hints on what you need to fix. If you are still stuck, you can check if anyone else ran into the same issue in our [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673). + +Assuming your app is now working, let's continue by migrating the rest of the app element tree to use the new system. + +First off we'll want to trim away any top-level elements in the app so that only the `routes` are left. For example, continuing where we left off with the following elements: + +```tsx title="in packages/app/src/App.tsx" +const legacyFeatures = convertLegacyApp( + <> + + + + {routes} + + , +); +``` + +You can remove all surrounding elements and just keep the `routes`: + +```tsx title="in packages/app/src/App.tsx" +const legacyFeatures = convertLegacyApp(routes); +``` + +This will remove many extension overrides that `convertLegacyApp` put in place, and switch over the shell of the app to the new system. This includes the root layout of the app along with the elements, router, and sidebar. The app will likely not look the same as before, and you'll need to refer to the [sidebar](#sidebar), [app root elements](#app-root-elements) and [app root wrappers](#app-root-wrappers) sections below for information on how to migrate those. + +Once that step is complete the work that remains is to migrate all of the [routes](#top-level-routes) and [entity pages](#entity-pages) in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the [plugin migration guide](../plugins/08-migrating.md). For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort. + +Once these migrations are complete you should be left with an empty `convertLegacyApp(...)` call that you can now remove, and your app should be fully migrated to the new system! 🎉 + +### Top-level Routes + +Your top-level routes are the routes directly under the `AppRouter` component with the `` element. In a small app they might look something like this: + +```tsx title="in packages/app/src/App.tsx" +const routes = ( + + } /> + } + > + {entityPage} + + } /> + } + /> + +); +``` + +Each of these routes need to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that **all routes from a single plugin must be migrated at once**. This is because plugins discovered from these legacy routes will override any plugin that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin. + +To migrate a route, you need to remove it from your list of routes and instead install the new version of the plugin in your app. Before doing this you should make sure that the plugin supports the new system. Let's remove the scaffolder route as an example: + +```tsx title="in packages/app/src/App.tsx" +const routes = ( + + } /> + } + > + {entityPage} + + {/* highlight-remove-next-line */} + } /> + } + /> + +); +``` + +If you are using [app feature discovery](../architecture/02-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually installed the new scaffolder plugin in your app through the `features` option of `createApp`. + +Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the `Route`, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the [separate section](#entity-pages). + +### Entity Pages + +The entity pages are typically defined in `packages/app/src/components/catalog` and rendered as a child of the `/catalog/:namespace/:kind/:name` route. The entity pages are typically quite large and bringing in content from quite a lot of different plugins. At the moment we do not provide a way to gradually migrate entity pages to the new system, although that is planned as a future improvement. This means that the entire entity page and all of its plugins need to be migrated at once, including any other usages of those plugins. + +### Sidebar + +New apps feature a built-in sidebar extension (`app/nav`) that will render all nav item extensions provided by plugins. This is a placeholder implementation and not intended as a long-term solution. In the future we will aim to provide a more flexible sidebar extension that allows for more customization out of the box. + +Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an extension override: + +```tsx +const nav = createExtension({ + namespace: 'app', + name: 'nav', + attachTo: { id: 'app/layout', input: 'nav' }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + {/* Sidebar contents from packages/app/src/components/Root.tsx go here */} + + ), + }; + }, +}); +``` + +### App Root Elements + +App root elements are React elements that are rendered adjacent to your current `Root` component. For example, in this snippet `AlertDisplay`, `OAuthRequestDialog` and `VisitListener` are all app root elements: + +```tsx +export default app.createRoot( + <> + {/* highlight-next-line */} + + {/* highlight-next-line */} + + + {/* highlight-next-line */} + + {routes} + + , +); +``` + +The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`. But, if you have your own custom root elements you will need to migrate them be extensions that you install in the app instead. Use `createAppRootElementExtension` to create said extension and then install it in the app. + +Whether the element used to be rendered as a child of the `AppRouter` or not doesn't matter. All new root app elements will be rendered as a child of the app router. + +### App Root Wrappers + +App root wrappers are React elements that are rendered as a parent of the current `Root` elements. For example, in this snippet the `CustomAppBarrier` is an app root wrapper: + +```tsx +export default app.createRoot( + <> + + + + {/* highlight-next-line */} + + {routes} + {/* highlight-next-line */} + + + , +); +``` + +Any app root wrapper needs to be migrated to be an extension instead, using `createAppRootWrapperExtension`. Note that if you have multiple wrappers they must be completely independent of each other, the order in which the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. + +## Tools + +- App Visualizer diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 155227f30a..6bbf9246ed 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -431,6 +431,11 @@ "frontend-system/architecture/references" ] }, + { + "type": "category", + "label": "Building Apps", + "items": ["frontend-system/building-apps/migrating"] + }, { "type": "category", "label": "Building Plugins", From 2a12834af7734b7c9a874c98a7c63c75547ed034 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 20 Jan 2024 09:46:16 +0100 Subject: [PATCH 440/483] Update docs/frontend-system/building-apps/08-migrating.md Co-authored-by: Phil Kuang Signed-off-by: Patrik Oldsberg --- docs/frontend-system/building-apps/08-migrating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 8efda0e211..a4e75f5390 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -74,7 +74,7 @@ export default app.createRoot(); We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyApp(...)`. We then pass the features returned by `convertLegacyApp` and forward them to the `features` option of the new `createApp`. -There is one more details that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather and a component, so we need to update our app `index.tsx` as follows: +There is one more detail that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather and a component, so we need to update our app `index.tsx` as follows: ```tsx title="in packages/app/src/index.tsx" import '@backstage/cli/asset-types'; From 26da51fbeb478b3f18e7cdd91f94baefaac37e69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 20 Jan 2024 10:02:21 +0100 Subject: [PATCH 441/483] docs/frontend-system: app migration review fixes Signed-off-by: Patrik Oldsberg --- docs/frontend-system/building-apps/08-migrating.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index a4e75f5390..f6804f6bb8 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -21,9 +21,9 @@ import { createApp } from '@backstage/app-defaults'; import { createApp } from '@backstage/frontend-app-api'; ``` -This immediate switch will lead to a lot of breakages that we need to fix. Let's start by addressing `app.createRoot(...)`, which no longer accepts any arguments. +This immediate switch will lead to a lot of breakages that we need to fix. -Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together to an extension tree in the new system. Much more responsibility has been shifted to plugins in the new system, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/01-index.md) section. +Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/01-index.md) section. Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once. @@ -102,13 +102,17 @@ For example, assuming you have a `lightTheme` extension that you want to add to ```ts const app = createApp({ features: [ + // highlight-add-start createExtensionOverrides({ extensions: [lightTheme], }), + // highlight-add-end ], }); ``` +You can then also add any additional extensions that you may need to create as part of this migration to the `extensions` array as well. + ### `apis` [Utility API](../utility-apis/01-index.md) factories are now installed as extensions instead. Pass the existing factory to `createApiExtension` and install it in the app. For more information, see the section on [configuring Utility APIs](../utility-apis/04-configuring.md). @@ -260,8 +264,8 @@ For example, if both the `catalogPlugin` and `scaffolderPlugin` are legacy plugi const app = createApp({ features: convertLegacyApp(...), bindRoutes({ bind }) { - bind(convertLegacyRouteRefs(catalogPlugin.createComponent), { - registerApi: convertLegacyRouteRef(scaffolderPlugin.routes.root), + bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), { + createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root), }); }, }); From 7139e71636011610d61bdbacafcb6d42b0996a3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 8 Feb 2024 12:50:14 +0100 Subject: [PATCH 442/483] microsite/sidebars: deduplicate frontend system app category Signed-off-by: Patrik Oldsberg --- microsite/sidebars.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 6bbf9246ed..a1d8cfbf88 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -431,11 +431,6 @@ "frontend-system/architecture/references" ] }, - { - "type": "category", - "label": "Building Apps", - "items": ["frontend-system/building-apps/migrating"] - }, { "type": "category", "label": "Building Plugins", @@ -452,7 +447,8 @@ "label": "Building Apps", "items": [ "frontend-system/building-apps/index", - "frontend-system/building-apps/built-in-extensions" + "frontend-system/building-apps/built-in-extensions", + "frontend-system/building-apps/migrating" ] }, { From d21fdd6447f86fdb0e11946086b9d3484c7881e0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 8 Feb 2024 13:24:18 +0100 Subject: [PATCH 443/483] WIP Signed-off-by: Patrik Oldsberg --- .../frontend-system/building-apps/01-index.md | 4 +- .../02-configuring-extensions.md | 97 +++++++++++++++++++ ...xtensions.md => 03-built-in-extensions.md} | 0 .../building-apps/08-migrating.md | 6 +- microsite/sidebars.json | 1 + 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 docs/frontend-system/building-apps/02-configuring-extensions.md rename docs/frontend-system/building-apps/{02-built-in-extensions.md => 03-built-in-extensions.md} (100%) diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index e23f628fb6..ea7fe20948 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -74,7 +74,7 @@ Remember that package extensions that are not auto-discovered must be manually a ### Configure extensions individually -It is possible to enable, disable and configure extensions individually in the `app-config.yaml` config file. To get familiar with what is available for app extensions personalization, go to the [built-in extensions](./02-built-in-extensions.md) documentation. For plugin customizations, we recommend that you read the instructions in each plugin's README file. +It is possible to enable, disable and configure extensions individually in the `app-config.yaml` config file. To get familiar with what is available for app extensions personalization, go to the [built-in extensions](./03-built-in-extensions.md) documentation. For plugin customizations, we recommend that you read the instructions in each plugin's README file. ### Customize or override built-in extensions @@ -139,7 +139,7 @@ const app = createApp({ // Calls an async utility method that fetches the config object from the server const config = await getConfigFromServer(); // Feel free to manipulate the config object before returning it - // A common example is conditionally modify the config based on the running enviroment + // A common example is conditionally modify the config based on the running environment return { config }; }, }); diff --git a/docs/frontend-system/building-apps/02-configuring-extensions.md b/docs/frontend-system/building-apps/02-configuring-extensions.md new file mode 100644 index 0000000000..57a2110da6 --- /dev/null +++ b/docs/frontend-system/building-apps/02-configuring-extensions.md @@ -0,0 +1,97 @@ +--- +id: configuring-extensions +title: Configuring Extensions in the App +sidebar_label: Configuring Extensions +# prettier-ignore +description: Documentation for how to configure extensions in a Backstage app +--- + +All extensions in a Backstage app can be configured through static configuration. This configuration is all done under a the `app.extensions` configuration key. For more general information on how to write configuration for Backstage, see the section on [writing configuration](../../conf/writing.md). + +## Extension Configuration Schema + +This section focuses on the format of the `app.extensions` configuration and the various shorthands that are available. + +The most complete and verbose format for configuring an individual extensions is as follows: + +```yaml +app: + extensions: + - : + attachTo: + id: + input: + disabled: + config: +``` + +All of the top-level fields are optional: `attachTo`, `disabled`, and `config`. Every extension implementation must provide defaults for all of these fields that will be used if they are not provided in the configuration. + +Note that `app.extensions` is always an array rather than an object. For example, the following is invalid: + +```yaml title="INVALID" +app: + extensions: + : # Invalid, this should be an array item, `app.extensions` is now an object + config: ... +``` + +In addition to this schema, there are a number of shorthands available: + +Rather than a full object, you can specify just the ID of the extension as a string. This is equivalent to setting `disabled` to `false`: + +```yaml +app: + extensions: + - ‘’ +``` + +You can enable/disable individual extension by ID, in this case the value is a boolean: + +```yaml +extensions: + - : +``` + +You can override the implementation of an extension by ID, in this case the value is a string: + +```yaml +extensions: + - : ‘’ +``` + +You can **create a new extension instance with a generated ID** by including an input name in the key: + +```yaml +extensions: + - /: + extension: + config: +``` + +This syntax is only for use in the app configuration itself, every extension provided by default from a plugin must have an explicit ID. For example, the following two configurations are equivalent, except that the former does not have an explicit instance ID: + +```yaml +extensions: + # Generated ID + - core.router/routes: + extension: '@backstage/plugin-tech-radar#TechRadarPage' + # Explicit ID + - tech-radar.page: + at: core.router/routes + extension: '@backstage/plugin-tech-radar#TechRadarPage' +``` + +Lastly, if you do not need to provide additional configuration, you can combine the key input format with the implementation value format as a shorthand for creating a new extension instance with a generated ID and no configuration: + +```yaml +extensions: + - /: ‘’ +``` + +For example: + +```yaml +extensions: + - core.router/routes: '@backstage/plugin-tech-radar#TechRadarPage' +``` diff --git a/docs/frontend-system/building-apps/02-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md similarity index 100% rename from docs/frontend-system/building-apps/02-built-in-extensions.md rename to docs/frontend-system/building-apps/03-built-in-extensions.md diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index f6804f6bb8..2018fecc8a 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -157,7 +157,7 @@ Declaring features flags in the app is no longer supported, move these declarati ### `components` -Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./index.md#TODO) for more information. +Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./01-index.md#configure-your-app) for more information. The `Router` component is now a built-in extension that you can override using `createRouterExtension`. @@ -433,7 +433,3 @@ export default app.createRoot( ``` Any app root wrapper needs to be migrated to be an extension instead, using `createAppRootWrapperExtension`. Note that if you have multiple wrappers they must be completely independent of each other, the order in which the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. - -## Tools - -- App Visualizer diff --git a/microsite/sidebars.json b/microsite/sidebars.json index a1d8cfbf88..2368ef25f7 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -447,6 +447,7 @@ "label": "Building Apps", "items": [ "frontend-system/building-apps/index", + "frontend-system/building-apps/configuring-extensions", "frontend-system/building-apps/built-in-extensions", "frontend-system/building-apps/migrating" ] From 8740058516d16d72b4f11f38f7f9f4f47cfa0d0c Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Wed, 28 Feb 2024 10:55:20 +0100 Subject: [PATCH 444/483] chore: regenerate api-report.md Signed-off-by: Marek Libra --- .../DatabaseNotificationsStore.test.ts | 4 ++-- .../database/DatabaseNotificationsStore.ts | 3 +-- .../src/service/router.ts | 6 +++-- plugins/notifications/api-report.md | 22 ++++++++++++++++--- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index 8abb0f1c06..c3f42767d7 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -228,7 +228,7 @@ describe.each(databases.eachSupportedId())( await insertNotification({ id: id1, ...testNotification, - created: new Date(Date.now() - 1 * 60 * 60 * 1000 /* an hour ago */), + created: new Date(now - 1 * 60 * 60 * 1000 /* an hour ago */), }); await insertNotification({ id: id2, @@ -270,7 +270,7 @@ describe.each(databases.eachSupportedId())( const notifications = await storage.getNotifications({ user, - createdAfter: new Date(Date.now() - 5 * 60 * 1000 /* 5mins */), + createdAfter: new Date(now - 5 * 60 * 1000 /* 5 mins */), }); expect(notifications.length).toBe(6); expect(notifications.at(0)?.id).toEqual(id7); diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 0ff1d10c56..4c2d2cc5e1 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -171,8 +171,7 @@ export class DatabaseNotificationsStore implements NotificationsStore { countOptions.sort = null; const notificationQuery = this.getNotificationsBaseQuery(countOptions); const response = await notificationQuery.count('* as CNT'); - const totalCount = Number.parseInt(response[0].CNT.toString(), 10); - return totalCount; + return Number(response[0].CNT); } async saveNotification(notification: Notification) { diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index b99350e9e4..9939040d37 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -212,8 +212,10 @@ export async function createRouter( opts.createdAfter = new Date(sinceEpoch); } - const notifications = await store.getNotifications(opts); - const totalCount = await store.getNotificationsCount(opts); + const [notifications, totalCount] = await Promise.all([ + store.getNotifications(opts), + store.getNotificationsCount(opts), + ]); res.send({ totalCount, notifications, diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 666daebdfe..caf9b4a2e5 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -14,6 +14,7 @@ import { Notification as Notification_2 } from '@backstage/plugin-notifications- import { NotificationStatus } from '@backstage/plugin-notifications-common'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TableProps } from '@backstage/core-components'; // @public (undocumented) export type GetNotificationsOptions = { @@ -24,6 +25,12 @@ export type GetNotificationsOptions = { createdAfter?: Date; }; +// @public (undocumented) +export type GetNotificationsResponse = { + notifications: Notification_2[]; + totalCount: number; +}; + // @public (undocumented) export interface NotificationsApi { // (undocumented) @@ -31,7 +38,7 @@ export interface NotificationsApi { // (undocumented) getNotifications( options?: GetNotificationsOptions, - ): Promise; + ): Promise; // (undocumented) getStatus(): Promise; // (undocumented) @@ -51,7 +58,7 @@ export class NotificationsClient implements NotificationsApi { // (undocumented) getNotifications( options?: GetNotificationsOptions, - ): Promise; + ): Promise; // (undocumented) getStatus(): Promise; // (undocumented) @@ -83,14 +90,23 @@ export const NotificationsTable: ({ notifications, onUpdate, setContainsText, + onPageChange, + onRowsPerPageChange, + page, + pageSize, + totalCount, }: NotificationsTableProps) => React_2.JSX.Element; // @public (undocumented) -export type NotificationsTableProps = { +export type NotificationsTableProps = Pick< + TableProps, + 'onPageChange' | 'onRowsPerPageChange' | 'page' | 'totalCount' +> & { isLoading?: boolean; notifications?: Notification_2[]; onUpdate: () => void; setContainsText: (search: string) => void; + pageSize: number; }; // @public (undocumented) From d03e4fef3802ab016faab8b008af93464939cf3f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Feb 2024 10:53:34 +0100 Subject: [PATCH 445/483] docs/frontend-system/building-apps: review fixes Signed-off-by: Patrik Oldsberg --- .../02-configuring-extensions.md | 90 +------------------ .../building-apps/08-migrating.md | 24 ++--- 2 files changed, 14 insertions(+), 100 deletions(-) diff --git a/docs/frontend-system/building-apps/02-configuring-extensions.md b/docs/frontend-system/building-apps/02-configuring-extensions.md index 57a2110da6..2fd4ce69fc 100644 --- a/docs/frontend-system/building-apps/02-configuring-extensions.md +++ b/docs/frontend-system/building-apps/02-configuring-extensions.md @@ -6,92 +6,4 @@ sidebar_label: Configuring Extensions description: Documentation for how to configure extensions in a Backstage app --- -All extensions in a Backstage app can be configured through static configuration. This configuration is all done under a the `app.extensions` configuration key. For more general information on how to write configuration for Backstage, see the section on [writing configuration](../../conf/writing.md). - -## Extension Configuration Schema - -This section focuses on the format of the `app.extensions` configuration and the various shorthands that are available. - -The most complete and verbose format for configuring an individual extensions is as follows: - -```yaml -app: - extensions: - - : - attachTo: - id: - input: - disabled: - config: -``` - -All of the top-level fields are optional: `attachTo`, `disabled`, and `config`. Every extension implementation must provide defaults for all of these fields that will be used if they are not provided in the configuration. - -Note that `app.extensions` is always an array rather than an object. For example, the following is invalid: - -```yaml title="INVALID" -app: - extensions: - : # Invalid, this should be an array item, `app.extensions` is now an object - config: ... -``` - -In addition to this schema, there are a number of shorthands available: - -Rather than a full object, you can specify just the ID of the extension as a string. This is equivalent to setting `disabled` to `false`: - -```yaml -app: - extensions: - - ‘’ -``` - -You can enable/disable individual extension by ID, in this case the value is a boolean: - -```yaml -extensions: - - : -``` - -You can override the implementation of an extension by ID, in this case the value is a string: - -```yaml -extensions: - - : ‘’ -``` - -You can **create a new extension instance with a generated ID** by including an input name in the key: - -```yaml -extensions: - - /: - extension: - config: -``` - -This syntax is only for use in the app configuration itself, every extension provided by default from a plugin must have an explicit ID. For example, the following two configurations are equivalent, except that the former does not have an explicit instance ID: - -```yaml -extensions: - # Generated ID - - core.router/routes: - extension: '@backstage/plugin-tech-radar#TechRadarPage' - # Explicit ID - - tech-radar.page: - at: core.router/routes - extension: '@backstage/plugin-tech-radar#TechRadarPage' -``` - -Lastly, if you do not need to provide additional configuration, you can combine the key input format with the implementation value format as a shorthand for creating a new extension instance with a generated ID and no configuration: - -```yaml -extensions: - - /: ‘’ -``` - -For example: - -```yaml -extensions: - - core.router/routes: '@backstage/plugin-tech-radar#TechRadarPage' -``` +TODO diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 2018fecc8a..98d8b3bab8 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -74,7 +74,7 @@ export default app.createRoot(); We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyApp(...)`. We then pass the features returned by `convertLegacyApp` and forward them to the `features` option of the new `createApp`. -There is one more detail that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather and a component, so we need to update our app `index.tsx` as follows: +There is one more detail that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather than a component, so we need to update our app `index.tsx` as follows: ```tsx title="in packages/app/src/index.tsx" import '@backstage/cli/asset-types'; @@ -161,7 +161,7 @@ Many app components are now installed as extensions instead using `createCompone The `Router` component is now a built-in extension that you can override using `createRouterExtension`. -The Sign-in page is now installed as an extension using the `createSignInPageExtension` instead. See the section on [configuring the sign-in page](./index.md#TODO) for more information. +The Sign-in page is now installed as an extension using the `createSignInPageExtension` instead. For example, the following sign-in page configuration: @@ -204,7 +204,7 @@ const signInPage = createSignInPageExtension({ ### `themes` -Themes are now installed as extensions instead using `createThemeExtension`. See the section on [configuring themes](./index.md#TODO) for more information. +Themes are now installed as extensions, using `createThemeExtension`. For example, the following theme configuration: @@ -216,7 +216,9 @@ const app = createApp({ title: 'Light', variant: 'light', Provider: ({ children }) => ( - + + {children} + ), }, ], @@ -273,11 +275,11 @@ const app = createApp({ ### `__experimentalTranslations` -Translations are now installed as extensions instead using `createTranslationExtension`. See the section on [configuring translations](./index.md#TODO) for more information. +Translations are now installed as extensions, using `createTranslationExtension`. ## Gradual Migration -After updating all `createApp` options as well as using `convertLegacyApp` to use our existing app structure, you should be able to start up the app and see that it still works. If that is not the case, make sure you read any errors messages that you may see in the app as they can provide hints on what you need to fix. If you are still stuck, you can check if anyone else ran into the same issue in our [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673). +After updating all `createApp` options as well as using `convertLegacyApp` to use your existing app structure, you should be able to start up the app and see that it still works. If that is not the case, make sure you read any error messages that you may see in the app as they can provide hints on what you need to fix. If you are still stuck, you can check if anyone else ran into the same issue in our [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673). Assuming your app is now working, let's continue by migrating the rest of the app element tree to use the new system. @@ -303,7 +305,7 @@ const legacyFeatures = convertLegacyApp(routes); This will remove many extension overrides that `convertLegacyApp` put in place, and switch over the shell of the app to the new system. This includes the root layout of the app along with the elements, router, and sidebar. The app will likely not look the same as before, and you'll need to refer to the [sidebar](#sidebar), [app root elements](#app-root-elements) and [app root wrappers](#app-root-wrappers) sections below for information on how to migrate those. -Once that step is complete the work that remains is to migrate all of the [routes](#top-level-routes) and [entity pages](#entity-pages) in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the [plugin migration guide](../plugins/08-migrating.md). For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort. +Once that step is complete the work that remains is to migrate all of the [routes](#top-level-routes) and [entity pages](#entity-pages) in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the [plugin migration guide](../building-plugins/05-migrating.md). For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort. Once these migrations are complete you should be left with an empty `convertLegacyApp(...)` call that you can now remove, and your app should be fully migrated to the new system! 🎉 @@ -330,7 +332,7 @@ const routes = ( ); ``` -Each of these routes need to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that **all routes from a single plugin must be migrated at once**. This is because plugins discovered from these legacy routes will override any plugin that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin. +Each of these routes needs to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that **all routes from a single plugin must be migrated at once**. This is because plugins discovered from these legacy routes will override any plugins that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin. To migrate a route, you need to remove it from your list of routes and instead install the new version of the plugin in your app. Before doing this you should make sure that the plugin supports the new system. Let's remove the scaffolder route as an example: @@ -354,7 +356,7 @@ const routes = ( ); ``` -If you are using [app feature discovery](../architecture/02-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually installed the new scaffolder plugin in your app through the `features` option of `createApp`. +If you are using [app feature discovery](../architecture/02-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the `features` option of `createApp`. Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the `Route`, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the [separate section](#entity-pages). @@ -408,7 +410,7 @@ export default app.createRoot( ); ``` -The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`. But, if you have your own custom root elements you will need to migrate them be extensions that you install in the app instead. Use `createAppRootElementExtension` to create said extension and then install it in the app. +The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`. But, if you have your own custom root elements you will need to migrate them to be extensions that you install in the app instead. Use `createAppRootElementExtension` to create said extension and then install it in the app. Whether the element used to be rendered as a child of the `AppRouter` or not doesn't matter. All new root app elements will be rendered as a child of the app router. @@ -432,4 +434,4 @@ export default app.createRoot( ); ``` -Any app root wrapper needs to be migrated to be an extension instead, using `createAppRootWrapperExtension`. Note that if you have multiple wrappers they must be completely independent of each other, the order in which the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. +Any app root wrapper needs to be migrated to be an extension, using `createAppRootWrapperExtension`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper. From 79cdee6cf3bb288131e36cfcc7946daa205cf528 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 20 Feb 2024 16:38:00 +0100 Subject: [PATCH 446/483] OWNERS.md: add notifications project area Signed-off-by: Patrik Oldsberg --- .github/CODEOWNERS | 5 +++++ OWNERS.md | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 75416ace16..4d18392945 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ yarn.lock @backstage/maintainers @backstage-service */yarn.lock @backstage/maintainers @backstage-service /.changeset/*.md +/beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers /docs/assets/search @backstage/discoverability-maintainers /docs/features/search @backstage/discoverability-maintainers /docs/features/techdocs @backstage/techdocs-maintainers @@ -66,6 +67,8 @@ yarn.lock @backstage/maintainers @backst /plugins/linguist @backstage/maintainers @backstage/reviewers @awanlin /plugins/linguist-backend @backstage/maintainers @backstage/reviewers @awanlin /plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin +/plugins/notifications @backstage/maintainers @backstage/notifications-maintainers +/plugins/notifications-* @backstage/maintainers @backstage/notifications-maintainers /plugins/octopus-deploy @backstage/maintainers @backstage/reviewers @jmezach /plugins/permission-* @backstage/permission-maintainers /plugins/playlist @backstage/maintainers @backstage/reviewers @kuangp @@ -76,6 +79,8 @@ yarn.lock @backstage/maintainers @backst /plugins/scaffolder-* @backstage/maintainers @backstage/reviewers @backstage/scaffolder-maintainers /plugins/search @backstage/discoverability-maintainers /plugins/search-* @backstage/discoverability-maintainers +/plugins/signals @backstage/maintainers @backstage/notifications-maintainers +/plugins/signals-* @backstage/maintainers @backstage/notifications-maintainers /plugins/sonarqube @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers /plugins/stack-overflow @backstage/discoverability-maintainers /plugins/stack-overflow-backend @backstage/discoverability-maintainers diff --git a/OWNERS.md b/OWNERS.md index 610d8bf984..4ef6b2ccab 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -118,6 +118,16 @@ Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugi | Philipp Hugenroth | Spotify | [tudi2d](https://github.com/tudi2d) | `tudi2d` | | Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | +### Notifications + +Team: @backstage/notifications-maintainers + +Scope: The Notifications and Signals plugins and libraries + +| Name | Organization | GitHub | Discord | +| ----------- | ------------ | ------------------------------------------- | --------- | +| Marek Libra | RedHat | [mareklibra](https://github.com/mareklibra) | `marekli` | + ### OpenAPI Tooling Team: @backstage/openapi-tooling-maintainers From c0f059728ab5bd41363041f3fa83a69391904522 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Wed, 28 Feb 2024 11:35:09 +0100 Subject: [PATCH 447/483] chore: use static uuids to siplify testing It's easier to debug failing tests with constant UUIDs. Signed-off-by: Marek Libra --- .../DatabaseNotificationsStore.test.ts | 90 ++++++++----------- 1 file changed, 36 insertions(+), 54 deletions(-) diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index c3f42767d7..0ee5958713 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -16,7 +16,6 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { DatabaseNotificationsStore } from './DatabaseNotificationsStore'; import { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; import { Notification } from '@backstage/plugin-notifications-common'; jest.setTimeout(60_000); @@ -54,6 +53,15 @@ const otherUserNotification: Partial = { user: 'user:default/jane.doe', }; +const id1 = '01e0871e-e60a-4f68-8110-5ae3513f992e'; +const id2 = '02e0871e-e60a-4f68-8110-5ae3513f992e'; +const id3 = '03e0871e-e60a-4f68-8110-5ae3513f992e'; +const id4 = '04e0871e-e60a-4f68-8110-5ae3513f992e'; +const id5 = '05e0871e-e60a-4f68-8110-5ae3513f992e'; +const id6 = '06e0871e-e60a-4f68-8110-5ae3513f992e'; +const id7 = '07e0871e-e60a-4f68-8110-5ae3513f992e'; +const id8 = '08e0871e-e60a-4f68-8110-5ae3513f992e'; + describe.each(databases.eachSupportedId())( 'DatabaseNotificationsStore (%s)', databaseId => { @@ -94,11 +102,9 @@ describe.each(databases.eachSupportedId())( describe('getNotifications', () => { it('should return all notifications for user', async () => { - const id1 = uuid(); - const id2 = uuid(); await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id3, ...otherUserNotification }); const notifications = await storage.getNotifications({ user }); expect(notifications.length).toBe(2); @@ -107,13 +113,10 @@ describe.each(databases.eachSupportedId())( }); it('should return read notifications for user', async () => { - const id1 = uuid(); - const id2 = uuid(); - const id3 = uuid(); await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); await insertNotification({ id: id3, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id4, ...otherUserNotification }); await storage.markRead({ ids: [id1, id3], user }); @@ -127,13 +130,10 @@ describe.each(databases.eachSupportedId())( }); it('should return unread notifications for user', async () => { - const id1 = uuid(); - const id2 = uuid(); - const id3 = uuid(); await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); await insertNotification({ id: id3, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id4, ...otherUserNotification }); await storage.markRead({ ids: [id1, id3], user }); @@ -146,13 +146,10 @@ describe.each(databases.eachSupportedId())( }); it('should return both read and unread notifications for user', async () => { - const id1 = uuid(); - const id2 = uuid(); - const id3 = uuid(); await insertNotification({ id: id1, ...testNotification }); await insertNotification({ id: id2, ...testNotification }); await insertNotification({ id: id3, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id4, ...otherUserNotification }); await storage.markRead({ ids: [id1, id3], user }); @@ -167,8 +164,6 @@ describe.each(databases.eachSupportedId())( }); it('should allow searching for notifications', async () => { - const id1 = uuid(); - const id2 = uuid(); await insertNotification({ id: id1, ...testNotification, @@ -179,7 +174,7 @@ describe.each(databases.eachSupportedId())( }, }); await insertNotification({ id: id2, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id3, ...otherUserNotification }); const notifications = await storage.getNotifications({ user, @@ -190,8 +185,6 @@ describe.each(databases.eachSupportedId())( }); it('should filter notifications based on created date', async () => { - const id1 = uuid(); - const id2 = uuid(); await insertNotification({ id: id1, ...testNotification, @@ -206,7 +199,7 @@ describe.each(databases.eachSupportedId())( }, created: new Date() /* now */, }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id3, ...otherUserNotification }); const notifications = await storage.getNotifications({ user, @@ -218,13 +211,8 @@ describe.each(databases.eachSupportedId())( it('should apply pagination', async () => { const now = Date.now(); - const id1 = uuid(); - const id2 = uuid(); - const id3 = uuid(); - const id4 = uuid(); - const id5 = uuid(); - const id6 = uuid(); - const id7 = uuid(); + const timeDelay = 5 * 1000; /* 5 secs */ + await insertNotification({ id: id1, ...testNotification, @@ -238,30 +226,30 @@ describe.each(databases.eachSupportedId())( await insertNotification({ id: id3, ...testNotification, - created: new Date(now + 1), + created: new Date(now - 5 * timeDelay), }); await insertNotification({ id: id4, ...testNotification, - created: new Date(now + 2), + created: new Date(now - 4 * timeDelay), }); await insertNotification({ id: id5, ...testNotification, - created: new Date(now + 3), + created: new Date(now - 3 * timeDelay), }); await insertNotification({ id: id6, ...testNotification, - created: new Date(now + 4), + created: new Date(now - 2 * timeDelay), }); await insertNotification({ id: id7, ...testNotification, - created: new Date(now + 5), + created: new Date(now - 1 * timeDelay), }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id8, ...otherUserNotification }); const allUserNotifications = await storage.getNotifications({ user, @@ -271,10 +259,14 @@ describe.each(databases.eachSupportedId())( const notifications = await storage.getNotifications({ user, createdAfter: new Date(now - 5 * 60 * 1000 /* 5 mins */), + // so far no pagination }); expect(notifications.length).toBe(6); - expect(notifications.at(0)?.id).toEqual(id7); - expect(notifications.at(1)?.id).toEqual(id6); + expect(notifications.at(0)?.id).toEqual(id2); + expect(notifications.at(1)?.id).toEqual(id7); + expect(notifications.at(2)?.id).toEqual(id6); + expect(notifications.at(3)?.id).toEqual(id5); + expect(notifications.at(4)?.id).toEqual(id4); const allUserNotificationsPageOne = await storage.getNotifications({ user, @@ -282,9 +274,9 @@ describe.each(databases.eachSupportedId())( offset: 0, }); expect(allUserNotificationsPageOne.length).toBe(3); - expect(allUserNotificationsPageOne.at(0)?.id).toEqual(id7); - expect(allUserNotificationsPageOne.at(1)?.id).toEqual(id6); - expect(allUserNotificationsPageOne.at(2)?.id).toEqual(id5); + expect(allUserNotificationsPageOne.at(0)?.id).toEqual(id2); + expect(allUserNotificationsPageOne.at(1)?.id).toEqual(id7); + expect(allUserNotificationsPageOne.at(2)?.id).toEqual(id6); const allUserNotificationsPageTwo = await storage.getNotifications({ user, @@ -292,23 +284,21 @@ describe.each(databases.eachSupportedId())( offset: 3, }); expect(allUserNotificationsPageTwo.length).toBe(3); - expect(allUserNotificationsPageTwo.at(0)?.id).toEqual(id4); - expect(allUserNotificationsPageTwo.at(1)?.id).toEqual(id3); - expect(allUserNotificationsPageTwo.at(2)?.id).toEqual(id2); + expect(allUserNotificationsPageTwo.at(0)?.id).toEqual(id5); + expect(allUserNotificationsPageTwo.at(1)?.id).toEqual(id4); + expect(allUserNotificationsPageTwo.at(2)?.id).toEqual(id3); }); }); describe('getStatus', () => { it('should return status for user', async () => { - const id1 = uuid(); - const id2 = uuid(); await insertNotification({ id: id1, ...testNotification, read: new Date(), }); await insertNotification({ id: id2, ...testNotification }); - await insertNotification({ id: uuid(), ...otherUserNotification }); + await insertNotification({ id: id3, ...otherUserNotification }); const status = await storage.getStatus({ user }); expect(status.read).toEqual(1); @@ -318,7 +308,6 @@ describe.each(databases.eachSupportedId())( describe('getExistingScopeNotification', () => { it('should return existing scope notification', async () => { - const id1 = uuid(); const notification: any = { ...testNotification, id: id1, @@ -343,7 +332,6 @@ describe.each(databases.eachSupportedId())( describe('restoreExistingNotification', () => { it('should return restore existing scope notification', async () => { - const id1 = uuid(); const notification: any = { ...testNotification, id: id1, @@ -377,7 +365,6 @@ describe.each(databases.eachSupportedId())( describe('getNotification', () => { it('should return notification by id', async () => { - const id1 = uuid(); await insertNotification({ id: id1, ...testNotification }); const notification = await storage.getNotification({ id: id1 }); @@ -387,7 +374,6 @@ describe.each(databases.eachSupportedId())( describe('markRead', () => { it('should mark notification read', async () => { - const id1 = uuid(); await insertNotification({ id: id1, ...testNotification }); await storage.markRead({ ids: [id1], user }); @@ -398,7 +384,6 @@ describe.each(databases.eachSupportedId())( describe('markUnread', () => { it('should mark notification unread', async () => { - const id1 = uuid(); await insertNotification({ id: id1, ...testNotification, @@ -413,7 +398,6 @@ describe.each(databases.eachSupportedId())( describe('markSaved', () => { it('should mark notification saved', async () => { - const id1 = uuid(); await insertNotification({ id: id1, ...testNotification }); await storage.markSaved({ ids: [id1], user }); @@ -424,7 +408,6 @@ describe.each(databases.eachSupportedId())( describe('markUnsaved', () => { it('should mark notification not saved', async () => { - const id1 = uuid(); await insertNotification({ id: id1, ...testNotification, @@ -439,7 +422,6 @@ describe.each(databases.eachSupportedId())( describe('saveNotification', () => { it('should store a notification', async () => { - const id1 = uuid(); await storage.saveNotification({ id: id1, user, From daf85dc4ab1d6f4251a5a7fe84e4cb9323f87d8a Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Wed, 28 Feb 2024 08:12:12 +0200 Subject: [PATCH 448/483] feat(signals,events)!: migrate signals to use events service + allow defining event paylod in events service Signed-off-by: Heikki Hellgren --- .changeset/slimy-falcons-poke.md | 7 ++++++ packages/backend/src/index.ts | 2 +- packages/backend/src/plugins/signals.ts | 2 +- .../src/service/standaloneServer.ts | 16 ++++++------- plugins/signals-backend/api-report.md | 4 ++-- plugins/signals-backend/src/plugin.ts | 15 +++++++++--- .../src/service/SignalManager.test.ts | 8 +++---- .../src/service/SignalManager.ts | 23 ++++++++----------- .../src/service/router.test.ts | 6 ++--- plugins/signals-backend/src/service/router.ts | 4 ++-- .../src/service/standaloneServer.ts | 18 +++++++-------- plugins/signals-node/api-report.md | 4 ++-- .../src/DefaultSignalService.test.ts | 6 ++--- .../signals-node/src/DefaultSignalService.ts | 9 ++++---- plugins/signals-node/src/lib.ts | 8 +++---- plugins/signals-node/src/types.ts | 4 ++-- 16 files changed, 72 insertions(+), 64 deletions(-) create mode 100644 .changeset/slimy-falcons-poke.md diff --git a/.changeset/slimy-falcons-poke.md b/.changeset/slimy-falcons-poke.md new file mode 100644 index 0000000000..9365c5f164 --- /dev/null +++ b/.changeset/slimy-falcons-poke.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-signals-backend': minor +'@backstage/plugin-signals-node': minor +--- + +BREAKING CHANGE: Migrates signals to use the `EventsService` and makes it mandatory diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 8931e6832b..d94b82211f 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -106,7 +106,7 @@ function makeCreateEnv(config: Config) { eventsService, ); const signalService = DefaultSignalService.create({ - eventBroker, + events: eventsService, }); root.info(`Created UrlReader ${reader}`); diff --git a/packages/backend/src/plugins/signals.ts b/packages/backend/src/plugins/signals.ts index 066fae74f8..d97d3fc170 100644 --- a/packages/backend/src/plugins/signals.ts +++ b/packages/backend/src/plugins/signals.ts @@ -22,7 +22,7 @@ export default async function createPlugin( ): Promise { return await createRouter({ logger: env.logger, - eventBroker: env.eventBroker, + events: env.events, identity: env.identity, discovery: env.discovery, }); diff --git a/plugins/notifications-backend/src/service/standaloneServer.ts b/plugins/notifications-backend/src/service/standaloneServer.ts index e2841543b1..c4927e8b38 100644 --- a/plugins/notifications-backend/src/service/standaloneServer.ts +++ b/plugins/notifications-backend/src/service/standaloneServer.ts @@ -33,9 +33,9 @@ import { } from '@backstage/catalog-client'; import { DefaultSignalService } from '@backstage/plugin-signals-node'; import { - EventBroker, EventParams, - EventSubscriber, + EventsService, + EventsServiceSubscribeOptions, } from '@backstage/plugin-events-node'; export interface ServerOptions { @@ -96,19 +96,17 @@ export async function startStandaloneServer( }, }; - const mockSubscribers: EventSubscriber[] = []; - const eventBroker: EventBroker = { + const mockSubscribers: EventsServiceSubscribeOptions[] = []; + const events: EventsService = { async publish(params: EventParams): Promise { mockSubscribers.forEach(sub => sub.onEvent(params)); }, - subscribe(...subscribers: EventSubscriber[]) { - subscribers.flat().forEach(subscriber => { - mockSubscribers.push(subscriber); - }); + async subscribe(subscription: EventsServiceSubscribeOptions) { + mockSubscribers.push(subscription); }, }; - const signalService = DefaultSignalService.create({ eventBroker }); + const signalService = DefaultSignalService.create({ events }); const router = await createRouter({ logger, diff --git a/plugins/signals-backend/api-report.md b/plugins/signals-backend/api-report.md index 5724c14aff..cad43bfaae 100644 --- a/plugins/signals-backend/api-report.md +++ b/plugins/signals-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -22,7 +22,7 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - eventBroker?: EventBroker; + events: EventsService; // (undocumented) identity: IdentityApi; // (undocumented) diff --git a/plugins/signals-backend/src/plugin.ts b/plugins/signals-backend/src/plugin.ts index f80a8a2dbd..2cd1917bfb 100644 --- a/plugins/signals-backend/src/plugin.ts +++ b/plugins/signals-backend/src/plugin.ts @@ -18,6 +18,7 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; /** * Signals backend plugin @@ -35,10 +36,17 @@ export const signalsPlugin = createBackendPlugin({ discovery: coreServices.discovery, userInfo: coreServices.userInfo, auth: coreServices.auth, - // TODO: EventBroker. It is optional for now but it's actually required so waiting for the new backend system - // for the events-backend for this to work. + events: eventsServiceRef, }, - async init({ httpRouter, logger, identity, discovery, userInfo, auth }) { + async init({ + httpRouter, + logger, + identity, + discovery, + userInfo, + auth, + events, + }) { httpRouter.use( await createRouter({ logger, @@ -46,6 +54,7 @@ export const signalsPlugin = createBackendPlugin({ discovery, userInfo, auth, + events, }), ); }, diff --git a/plugins/signals-backend/src/service/SignalManager.test.ts b/plugins/signals-backend/src/service/SignalManager.test.ts index cf19f36224..e6d0cab21b 100644 --- a/plugins/signals-backend/src/service/SignalManager.test.ts +++ b/plugins/signals-backend/src/service/SignalManager.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { WebSocket } from 'ws'; -import { EventSubscriber } from '@backstage/plugin-events-node'; +import { EventsServiceSubscribeOptions } from '@backstage/plugin-events-node'; import { SignalManager } from './SignalManager'; import { getVoidLogger } from '@backstage/backend-common'; @@ -56,15 +56,15 @@ class MockWebSocket { describe('SignalManager', () => { let onEvent: Function; - const mockEventBroker = { + const mockEvents = { publish: async () => {}, - subscribe: (subscriber: EventSubscriber) => { + subscribe: async (subscriber: EventsServiceSubscribeOptions) => { onEvent = subscriber.onEvent; }, }; const manager = SignalManager.create({ - eventBroker: mockEventBroker, + events: mockEvents, logger: getVoidLogger(), }); diff --git a/plugins/signals-backend/src/service/SignalManager.ts b/plugins/signals-backend/src/service/SignalManager.ts index c31c7869bd..775af143f4 100644 --- a/plugins/signals-backend/src/service/SignalManager.ts +++ b/plugins/signals-backend/src/service/SignalManager.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventBroker, EventParams } from '@backstage/plugin-events-node'; +import { EventParams, EventsService } from '@backstage/plugin-events-node'; import { SignalPayload } from '@backstage/plugin-signals-node'; import { RawData, WebSocket } from 'ws'; import { v4 as uuid } from 'uuid'; @@ -38,8 +38,7 @@ export type SignalConnection = { * @internal */ export type SignalManagerOptions = { - // TODO: Remove optional when events-backend can offer this service - eventBroker?: EventBroker; + events: EventsService; logger: LoggerService; }; @@ -49,7 +48,7 @@ export class SignalManager { string, SignalConnection >(); - private eventBroker?: EventBroker; + private events: EventsService; private logger: LoggerService; static create(options: SignalManagerOptions) { @@ -57,12 +56,13 @@ export class SignalManager { } private constructor(options: SignalManagerOptions) { - ({ eventBroker: this.eventBroker, logger: this.logger } = options); + ({ events: this.events, logger: this.logger } = options); - this.eventBroker?.subscribe({ - supportsEventTopics: () => ['signals'], - onEvent: (params: EventParams) => - this.onEventBrokerEvent(params), + this.events.subscribe({ + id: 'signals', + topics: ['signals'], + onEvent: (params: EventParams) => + this.onEventBrokerEvent(params.eventPayload as SignalPayload), }); } @@ -126,10 +126,7 @@ export class SignalManager { } } - private async onEventBrokerEvent( - params: EventParams, - ): Promise { - const { eventPayload } = params; + private async onEventBrokerEvent(eventPayload: SignalPayload): Promise { if (!eventPayload.channel || !eventPayload.message) { return; } diff --git a/plugins/signals-backend/src/service/router.test.ts b/plugins/signals-backend/src/service/router.test.ts index 64367ee779..bbcf506d19 100644 --- a/plugins/signals-backend/src/service/router.test.ts +++ b/plugins/signals-backend/src/service/router.test.ts @@ -21,11 +21,11 @@ import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { UserInfoService } from '@backstage/backend-plugin-api'; -const eventBrokerMock: jest.Mocked = { +const eventsServiceMock: jest.Mocked = { subscribe: jest.fn(), publish: jest.fn(), }; @@ -50,7 +50,7 @@ describe('createRouter', () => { const router = await createRouter({ logger: getVoidLogger(), identity: identityApiMock, - eventBroker: eventBrokerMock, + events: eventsServiceMock, discovery, userInfo, }); diff --git a/plugins/signals-backend/src/service/router.ts b/plugins/signals-backend/src/service/router.ts index 5a6f4469b2..fab706e878 100644 --- a/plugins/signals-backend/src/service/router.ts +++ b/plugins/signals-backend/src/service/router.ts @@ -30,14 +30,14 @@ import * as https from 'https'; import http, { IncomingMessage } from 'http'; import { SignalManager } from './SignalManager'; import { IdentityApi } from '@backstage/plugin-auth-node'; -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { WebSocket, WebSocketServer } from 'ws'; import { Duplex } from 'stream'; /** @public */ export interface RouterOptions { logger: LoggerService; - eventBroker?: EventBroker; + events: EventsService; identity: IdentityApi; discovery: PluginEndpointDiscovery; auth?: AuthService; diff --git a/plugins/signals-backend/src/service/standaloneServer.ts b/plugins/signals-backend/src/service/standaloneServer.ts index f88848fffc..b143ce3684 100644 --- a/plugins/signals-backend/src/service/standaloneServer.ts +++ b/plugins/signals-backend/src/service/standaloneServer.ts @@ -24,9 +24,9 @@ import { createRouter } from './router'; import { DefaultSignalService } from '@backstage/plugin-signals-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { - EventBroker, EventParams, - EventSubscriber, + EventsService, + EventsServiceSubscribeOptions, } from '@backstage/plugin-events-node'; import { BackstageCredentials, @@ -53,20 +53,18 @@ export async function startStandaloneServer( issuer: await discovery.getExternalBaseUrl('auth'), }); - const mockSubscribers: EventSubscriber[] = []; - const eventBroker: EventBroker = { + const mockSubscribers: EventsServiceSubscribeOptions[] = []; + const events: EventsService = { async publish(params: EventParams): Promise { mockSubscribers.forEach(sub => sub.onEvent(params)); }, - subscribe(...subscribers: EventSubscriber[]) { - subscribers.flat().forEach(subscriber => { - mockSubscribers.push(subscriber); - }); + async subscribe(subscription: EventsServiceSubscribeOptions) { + mockSubscribers.push(subscription); }, }; const signals = DefaultSignalService.create({ - eventBroker, + events, }); const userInfo: UserInfoService = { @@ -81,7 +79,7 @@ export async function startStandaloneServer( const router = await createRouter({ logger, identity, - eventBroker, + events, discovery, userInfo, }); diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index 9d67e24732..21c35e5e36 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { JsonObject } from '@backstage/types'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -35,7 +35,7 @@ export const signalService: ServiceRef; // @public (undocumented) export type SignalServiceOptions = { - eventBroker?: EventBroker; + events: EventsService; }; // (No @packageDocumentation comment for this package) diff --git a/plugins/signals-node/src/DefaultSignalService.test.ts b/plugins/signals-node/src/DefaultSignalService.test.ts index 9938c84c86..0b868d8ec4 100644 --- a/plugins/signals-node/src/DefaultSignalService.test.ts +++ b/plugins/signals-node/src/DefaultSignalService.test.ts @@ -16,12 +16,12 @@ import { DefaultSignalService } from './DefaultSignalService'; describe('DefaultSignalService', () => { - const mockEventBroker = { + const mockEvents = { publish: jest.fn(), subscribe: jest.fn(), }; - const service = DefaultSignalService.create({ eventBroker: mockEventBroker }); + const service = DefaultSignalService.create({ events: mockEvents }); it('should publish signal', () => { const signal = { @@ -30,7 +30,7 @@ describe('DefaultSignalService', () => { message: { msg: 'hello world' }, }; service.publish(signal); - expect(mockEventBroker.publish).toHaveBeenCalledWith({ + expect(mockEvents.publish).toHaveBeenCalledWith({ topic: 'signals', eventPayload: signal, }); diff --git a/plugins/signals-node/src/DefaultSignalService.ts b/plugins/signals-node/src/DefaultSignalService.ts index d1bc14e166..e3ffba8d7b 100644 --- a/plugins/signals-node/src/DefaultSignalService.ts +++ b/plugins/signals-node/src/DefaultSignalService.ts @@ -13,22 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { SignalPayload, SignalServiceOptions } from './types'; import { SignalService } from './SignalService'; import { JsonObject } from '@backstage/types'; /** @public */ export class DefaultSignalService implements SignalService { - // TODO: Remove this to be optional when events-backend has eventBroker as service - private eventBroker?: EventBroker; + private events: EventsService; static create(options: SignalServiceOptions) { return new DefaultSignalService(options); } private constructor(options: SignalServiceOptions) { - ({ eventBroker: this.eventBroker } = options); + ({ events: this.events } = options); } /** @@ -38,7 +37,7 @@ export class DefaultSignalService implements SignalService { async publish( signal: SignalPayload, ) { - await this.eventBroker?.publish({ + await this.events.publish({ topic: 'signals', eventPayload: signal, }); diff --git a/plugins/signals-node/src/lib.ts b/plugins/signals-node/src/lib.ts index 095a2f085d..7e811cb10e 100644 --- a/plugins/signals-node/src/lib.ts +++ b/plugins/signals-node/src/lib.ts @@ -19,6 +19,7 @@ import { } from '@backstage/backend-plugin-api'; import { DefaultSignalService } from './DefaultSignalService'; import { SignalService } from './SignalService'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; /** @public */ export const signalService = createServiceRef({ @@ -28,11 +29,10 @@ export const signalService = createServiceRef({ createServiceFactory({ service, deps: { - // TODO: EventBroker. It is optional for now but it's actually required so waiting for the new backend system - // for the events-backend for this to work. + events: eventsServiceRef, }, - factory({}) { - return DefaultSignalService.create({}); + factory({ events }) { + return DefaultSignalService.create({ events }); }, }), }); diff --git a/plugins/signals-node/src/types.ts b/plugins/signals-node/src/types.ts index ec6e815b29..efd45c56c0 100644 --- a/plugins/signals-node/src/types.ts +++ b/plugins/signals-node/src/types.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsService } from '@backstage/plugin-events-node'; import { JsonObject } from '@backstage/types'; /** * @public */ export type SignalServiceOptions = { - eventBroker?: EventBroker; + events: EventsService; }; /** @public */ From dd44066211afa60f834a5c670364167e73de83ba Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 28 Feb 2024 15:48:12 +0100 Subject: [PATCH 449/483] blog: add security notice blog Signed-off-by: blam --- microsite/blog/2024-02-28-security-notice.mdx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 microsite/blog/2024-02-28-security-notice.mdx diff --git a/microsite/blog/2024-02-28-security-notice.mdx b/microsite/blog/2024-02-28-security-notice.mdx new file mode 100644 index 0000000000..4605aa04b2 --- /dev/null +++ b/microsite/blog/2024-02-28-security-notice.mdx @@ -0,0 +1,24 @@ +--- +title: "CVE-2024-26150: Keeping Backstage safe and secure" +author: Ben Lambert, Spotify & Sam Nixon, Roadie +--- + +**TL;DR**: For the Backstage maintainers, ensuring that the project is secure for every adopter and end user is one of our top priorities. +With the recent discovery of [CVE-2024-26150](https://www.cve.org/CVERecord?id=CVE-2024-26150), we've shipped fixes for versions > v1.15.0. +Please update your Backstage instance. + +![Backstage Security Audit & Updates](assets/22-08-23/backstage-security-audit.png) + +{/* truncate */} + +Last week we were notified by [Roadie](https://roadie.io/) of a potential security vulnerability in the scaffolder that they had discovered during a third-party security audit. +Roadie are running their scaffolder tasks in an isolated, ephemeral container to mitigate these kinds of issues, but as good citizens of the Backstage community, they alerted the Backstage maintainer team to make sure a fix was applied to the upstream project. +Upon further investigation, it turned out that this was a more widespread issue with the `resolveSafeChildPath` utility from the `@backstage/backend-common` package, which is used to prevent path traversal exploits. +This issue has now been fixed, and also backported to cover releases almost a year ago. + +Please make sure that you have updated your Backstage instance to the latest v.1.23.2, or that you are using `@backstage/backend-common` `v0.21.1`, `v0.19.10`, or `v0.20.2`. +If you are building backend plugins for Backstage and do any local file operations, make sure you use the `resolveSafeChildPath` utility, as this check is quite tricky to implement correctly 😊 + +Thanks to Roadie and the team for reporting this issue, and making the Backstage community safe! + +For more information you can check out the [security advisory](https://github.com/backstage/backstage/security/advisories/GHSA-2fc9-xpp8-2g9h). From 83e7ec63222bc41b26f9dd3e445e2ea397e3ecad Mon Sep 17 00:00:00 2001 From: nikolar Date: Wed, 28 Feb 2024 09:59:13 -0800 Subject: [PATCH 450/483] add review suggestion Signed-off-by: nikolar --- .changeset/perfect-shoes-arrive.md | 2 -- .../FeedbackResponseTable/FeedbackResponseTable.tsx | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/perfect-shoes-arrive.md b/.changeset/perfect-shoes-arrive.md index eaff65c082..93862df24d 100644 --- a/.changeset/perfect-shoes-arrive.md +++ b/.changeset/perfect-shoes-arrive.md @@ -2,6 +2,4 @@ '@backstage/plugin-entity-feedback': patch --- - - Remove empty Chip in `FeedbackResponseTable.tsx` when there is no response, and fix typo in Feedback Dialog Box. diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx index 2a756ee105..8cede00878 100644 --- a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx @@ -80,11 +80,13 @@ export const FeedbackResponseTable = (props: FeedbackResponseTableProps) => { width: '35%', render: (response: ResponseRow) => ( <> - {response?.response && - response.response.length > 0 && - response.response - ?.split(',') - .map(res => )} + {(response.response || '') + .split(',') + .map(v => v.trim()) // removes whitespace + .filter(Boolean) // removes accidental empty entries + .map(res => ( + + ))} ), }, From c75996741b5d6b99d1b484277af3277b05e431f8 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 28 Feb 2024 19:07:36 +0100 Subject: [PATCH 451/483] Update microsite/blog/2024-02-28-security-notice.mdx 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 Signed-off-by: blam --- microsite/blog/2024-02-28-security-notice.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2024-02-28-security-notice.mdx b/microsite/blog/2024-02-28-security-notice.mdx index 4605aa04b2..4f987b90a4 100644 --- a/microsite/blog/2024-02-28-security-notice.mdx +++ b/microsite/blog/2024-02-28-security-notice.mdx @@ -14,7 +14,7 @@ Please update your Backstage instance. Last week we were notified by [Roadie](https://roadie.io/) of a potential security vulnerability in the scaffolder that they had discovered during a third-party security audit. Roadie are running their scaffolder tasks in an isolated, ephemeral container to mitigate these kinds of issues, but as good citizens of the Backstage community, they alerted the Backstage maintainer team to make sure a fix was applied to the upstream project. Upon further investigation, it turned out that this was a more widespread issue with the `resolveSafeChildPath` utility from the `@backstage/backend-common` package, which is used to prevent path traversal exploits. -This issue has now been fixed, and also backported to cover releases almost a year ago. +This issue has now been fixed, and also backported to cover releases up to almost a year old. Please make sure that you have updated your Backstage instance to the latest v.1.23.2, or that you are using `@backstage/backend-common` `v0.21.1`, `v0.19.10`, or `v0.20.2`. If you are building backend plugins for Backstage and do any local file operations, make sure you use the `resolveSafeChildPath` utility, as this check is quite tricky to implement correctly 😊 From 938ef3a8df7ed9d52088b8c93e860cb028706739 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 29 Feb 2024 07:27:10 +0100 Subject: [PATCH 452/483] docs: run prettier Signed-off-by: Vincenzo Scamporlino --- microsite/blog/2024-02-28-security-notice.mdx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/microsite/blog/2024-02-28-security-notice.mdx b/microsite/blog/2024-02-28-security-notice.mdx index 4f987b90a4..6d174bdb3c 100644 --- a/microsite/blog/2024-02-28-security-notice.mdx +++ b/microsite/blog/2024-02-28-security-notice.mdx @@ -1,22 +1,22 @@ --- -title: "CVE-2024-26150: Keeping Backstage safe and secure" +title: 'CVE-2024-26150: Keeping Backstage safe and secure' author: Ben Lambert, Spotify & Sam Nixon, Roadie --- -**TL;DR**: For the Backstage maintainers, ensuring that the project is secure for every adopter and end user is one of our top priorities. -With the recent discovery of [CVE-2024-26150](https://www.cve.org/CVERecord?id=CVE-2024-26150), we've shipped fixes for versions > v1.15.0. +**TL;DR**: For the Backstage maintainers, ensuring that the project is secure for every adopter and end user is one of our top priorities. +With the recent discovery of [CVE-2024-26150](https://www.cve.org/CVERecord?id=CVE-2024-26150), we've shipped fixes for versions > v1.15.0. Please update your Backstage instance. ![Backstage Security Audit & Updates](assets/22-08-23/backstage-security-audit.png) {/* truncate */} -Last week we were notified by [Roadie](https://roadie.io/) of a potential security vulnerability in the scaffolder that they had discovered during a third-party security audit. -Roadie are running their scaffolder tasks in an isolated, ephemeral container to mitigate these kinds of issues, but as good citizens of the Backstage community, they alerted the Backstage maintainer team to make sure a fix was applied to the upstream project. -Upon further investigation, it turned out that this was a more widespread issue with the `resolveSafeChildPath` utility from the `@backstage/backend-common` package, which is used to prevent path traversal exploits. +Last week we were notified by [Roadie](https://roadie.io/) of a potential security vulnerability in the scaffolder that they had discovered during a third-party security audit. +Roadie are running their scaffolder tasks in an isolated, ephemeral container to mitigate these kinds of issues, but as good citizens of the Backstage community, they alerted the Backstage maintainer team to make sure a fix was applied to the upstream project. +Upon further investigation, it turned out that this was a more widespread issue with the `resolveSafeChildPath` utility from the `@backstage/backend-common` package, which is used to prevent path traversal exploits. This issue has now been fixed, and also backported to cover releases up to almost a year old. -Please make sure that you have updated your Backstage instance to the latest v.1.23.2, or that you are using `@backstage/backend-common` `v0.21.1`, `v0.19.10`, or `v0.20.2`. +Please make sure that you have updated your Backstage instance to the latest v.1.23.2, or that you are using `@backstage/backend-common` `v0.21.1`, `v0.19.10`, or `v0.20.2`. If you are building backend plugins for Backstage and do any local file operations, make sure you use the `resolveSafeChildPath` utility, as this check is quite tricky to implement correctly 😊 Thanks to Roadie and the team for reporting this issue, and making the Backstage community safe! From bbd1fe19ab7a8a25e5268afc7cba421c15d23734 Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Thu, 29 Feb 2024 10:41:34 +0100 Subject: [PATCH 453/483] Made "checkpoint" on scaffolder action context non-optional (#23291) * Made "checkpoint" on scaffolder action context non-optional Signed-off-by: bnechyporenko --- .changeset/thirty-bikes-stare.md | 7 +++++++ .../src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts | 6 +++--- .../src/actions/mockActionConext.ts | 1 + plugins/scaffolder-node/api-report.md | 2 +- plugins/scaffolder-node/src/actions/types.ts | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .changeset/thirty-bikes-stare.md diff --git a/.changeset/thirty-bikes-stare.md b/.changeset/thirty-bikes-stare.md new file mode 100644 index 0000000000..d8249299a7 --- /dev/null +++ b/.changeset/thirty-bikes-stare.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-node-test-utils': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Made "checkpoint" on scaffolder action context non-optional diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 420ec69427..f5bbee2831 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -138,13 +138,13 @@ describe('NunjucksWorkflowRunner', () => { id: 'checkpoints-action', description: 'Mock action with checkpoints', handler: async ctx => { - const key1 = await ctx.checkpoint?.('key1', async () => { + const key1 = await ctx.checkpoint('key1', async () => { return 'updated'; }); - const key2 = await ctx.checkpoint?.('key2', async () => { + const key2 = await ctx.checkpoint('key2', async () => { return 'updated'; }); - const key3 = await ctx.checkpoint?.('key3', async () => { + const key3 = await ctx.checkpoint('key3', async () => { return 'updated'; }); diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts index 33a40ad02c..f9cd556fd9 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts @@ -38,6 +38,7 @@ export const createMockActionContext = < output: jest.fn(), createTemporaryDirectory: jest.fn(), input: {} as TActionInput, + checkpoint: jest.fn(), }; const createDefaultWorkspace = () => ({ diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 76e22c0c5a..ba914f8f87 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -30,7 +30,7 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint?( + checkpoint( key: string, fn: () => Promise, ): Promise; diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index 678d6f87d7..5abeb29a2b 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -35,7 +35,7 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint?( + checkpoint( key: string, fn: () => Promise, ): Promise; From f235ca7982d3e81afce8bfb6a29f82a0d21c8589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 29 Feb 2024 14:10:39 +0100 Subject: [PATCH 454/483] fix filter in createConfigSecretEnumerator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/twelve-berries-cross.md | 5 +++++ packages/backend-app-api/src/config/config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/twelve-berries-cross.md diff --git a/.changeset/twelve-berries-cross.md b/.changeset/twelve-berries-cross.md new file mode 100644 index 0000000000..9862f465d2 --- /dev/null +++ b/.changeset/twelve-berries-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Make sure to not filter out schemas in `createConfigSecretEnumerator` diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index 8f98c5597c..7673ad3871 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -42,7 +42,7 @@ export async function createConfigSecretEnumerator(options: { const schema = options.schema ?? (await loadConfigSchema({ - dependencies: packages.map(p => p.packageJson.name).filter(() => false), + dependencies: packages.map(p => p.packageJson.name), })); return (config: Config) => { From e1f73d091ef0458e7658171a62620b6293b8d66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 29 Feb 2024 14:33:04 +0100 Subject: [PATCH 455/483] Added config.d.ts entry with secrets for the shared auth block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/proud-socks-hear.md | 5 ++++ .../config.d.ts | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 .changeset/proud-socks-hear.md diff --git a/.changeset/proud-socks-hear.md b/.changeset/proud-socks-hear.md new file mode 100644 index 0000000000..20764ae8a0 --- /dev/null +++ b/.changeset/proud-socks-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Added config.d.ts entry with secrets for the shared auth block diff --git a/plugins/search-backend-module-elasticsearch/config.d.ts b/plugins/search-backend-module-elasticsearch/config.d.ts index b798240cef..401e55f792 100644 --- a/plugins/search-backend-module-elasticsearch/config.d.ts +++ b/plugins/search-backend-module-elasticsearch/config.d.ts @@ -212,6 +212,33 @@ export interface Config { }; } ); + + /** + * Authentication credentials for ElasticSearch. These are fallback + * credentials - in most cases, for known specific ES implementations, the + * respective auth block inside the clientOptions above will be used. + * + * If both ApiKey/Bearer token and username+password is provided, tokens + * take precedence + */ + auth?: + | { + username: string; + + /** + * @visibility secret + */ + password: string; + } + | { + /** + * Base64 Encoded API key to be used to connect to the cluster. + * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html + * + * @visibility secret + */ + apiKey: string; + }; }; }; } From 8bfcc502f50cbc63897ef039455228bc9bff47a6 Mon Sep 17 00:00:00 2001 From: Ilya Savich Date: Thu, 29 Feb 2024 15:10:32 +0100 Subject: [PATCH 456/483] Send failed process status if knip fails Signed-off-by: Ilya Savich --- .changeset/cool-clouds-jump.md | 5 +++++ .../src/commands/knip-reports/knip-extractor.ts | 2 ++ .../src/commands/knip-reports/knip-reports.ts | 13 +++++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .changeset/cool-clouds-jump.md diff --git a/.changeset/cool-clouds-jump.md b/.changeset/cool-clouds-jump.md new file mode 100644 index 0000000000..c94ab69a38 --- /dev/null +++ b/.changeset/cool-clouds-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Fix knip-report command to send 1 exit status in case of fail 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 426895700b..50c429f986 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts @@ -178,5 +178,7 @@ export async function runKnipReports({ const fullDir = cliPaths.resolveTargetRoot(packageDir); cleanKnipConfig({ packageDir: fullDir }); }); + + throw e; } } diff --git a/packages/repo-tools/src/commands/knip-reports/knip-reports.ts b/packages/repo-tools/src/commands/knip-reports/knip-reports.ts index 48ffa6cbdb..79872b4cae 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-reports.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-reports.ts @@ -46,9 +46,14 @@ export const buildKnipReports = async (paths: string[] = [], opts: Options) => { if (selectedPackageDirs.length > 0) { console.log('# Generating package knip reports'); - await runKnipReports({ - packageDirs: selectedPackageDirs, - isLocalBuild: !isCiBuild, - }); + + try { + await runKnipReports({ + packageDirs: selectedPackageDirs, + isLocalBuild: !isCiBuild, + }); + } catch (e) { + process.exit(1); + } } }; From a790a3dfa0fa2c8036e4618121a62a7b20ee4cad Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 29 Feb 2024 17:49:48 +0200 Subject: [PATCH 457/483] feat: move notifications origin resolving to backend Signed-off-by: Heikki Hellgren --- .changeset/fair-cheetahs-suffer.md | 7 +++ plugins/notifications-backend/src/plugin.ts | 9 ++-- .../src/service/router.test.ts | 36 ++++------------ .../src/service/router.ts | 43 ++++++++----------- .../src/service/standaloneServer.ts | 20 ++++++++- plugins/notifications-common/api-report.md | 2 +- plugins/notifications-common/src/types.ts | 2 +- plugins/notifications-node/api-report.md | 1 - plugins/notifications-node/src/lib.ts | 4 +- .../DefaultNotificationService.test.ts | 5 +-- .../src/service/DefaultNotificationService.ts | 15 ++----- 11 files changed, 62 insertions(+), 82 deletions(-) create mode 100644 .changeset/fair-cheetahs-suffer.md diff --git a/.changeset/fair-cheetahs-suffer.md b/.changeset/fair-cheetahs-suffer.md new file mode 100644 index 0000000000..a41237acaa --- /dev/null +++ b/.changeset/fair-cheetahs-suffer.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-common': patch +'@backstage/plugin-notifications-node': patch +--- + +Move notification origin resolving to backend with new auth diff --git a/plugins/notifications-backend/src/plugin.ts b/plugins/notifications-backend/src/plugin.ts index 1d81818e30..29d444efd3 100644 --- a/plugins/notifications-backend/src/plugin.ts +++ b/plugins/notifications-backend/src/plugin.ts @@ -61,22 +61,20 @@ export const notificationsPlugin = createBackendPlugin({ deps: { auth: coreServices.auth, httpAuth: coreServices.httpAuth, + userInfo: coreServices.userInfo, httpRouter: coreServices.httpRouter, logger: coreServices.logger, - identity: coreServices.identity, database: coreServices.database, - tokenManager: coreServices.tokenManager, discovery: coreServices.discovery, signals: signalService, }, async init({ auth, httpAuth, + userInfo, httpRouter, logger, - identity, database, - tokenManager, discovery, signals, }) { @@ -84,10 +82,9 @@ export const notificationsPlugin = createBackendPlugin({ await createRouter({ auth, httpAuth, + userInfo, logger, - identity, database, - tokenManager, discovery, signalService: signals, processors: processingExtensions.processors, diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 3152b1b07f..13cb5e2f54 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -17,16 +17,14 @@ import { DatabaseManager, getVoidLogger, PluginDatabaseManager, - PluginEndpointDiscovery, - TokenManager, } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; -import { IdentityApi } from '@backstage/plugin-auth-node'; import { ConfigReader } from '@backstage/config'; import { SignalService } from '@backstage/plugin-signals-node'; +import { mockServices } from '@backstage/backend-test-utils'; function createDatabase(): PluginDatabaseManager { return DatabaseManager.fromConfig( @@ -44,40 +42,24 @@ function createDatabase(): PluginDatabaseManager { describe('createRouter', () => { let app: express.Express; - const identityMock: IdentityApi = { - async getIdentity() { - return { - identity: { - type: 'user', - ownershipEntityRefs: [], - userEntityRef: 'user:default/guest', - }, - token: 'no-token', - }; - }, - }; - const mockedTokenManager: jest.Mocked = { - getToken: jest.fn(), - authenticate: jest.fn(), - }; - - const discovery: jest.Mocked = { - getBaseUrl: jest.fn(), - getExternalBaseUrl: jest.fn(), - }; - const signalService: jest.Mocked = { publish: jest.fn(), }; + const discovery = mockServices.discovery(); + const userInfo = mockServices.userInfo(); + const httpAuth = mockServices.httpAuth(); + const auth = mockServices.auth(); + beforeAll(async () => { const router = await createRouter({ logger: getVoidLogger(), - identity: identityMock, database: createDatabase(), - tokenManager: mockedTokenManager, discovery, signalService, + userInfo, + httpAuth, + auth, }); app = express().use(router); }); diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 78d44dcd44..221606e2da 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -13,15 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - createLegacyAuthAdapters, - errorHandler, - PluginDatabaseManager, - TokenManager, -} from '@backstage/backend-common'; +import { errorHandler, PluginDatabaseManager } from '@backstage/backend-common'; import express, { Request } from 'express'; import Router from 'express-promise-router'; -import { IdentityApi } from '@backstage/plugin-auth-node'; import { DatabaseNotificationsStore, NotificationGetOptions, @@ -36,12 +30,13 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { NotificationProcessor } from '@backstage/plugin-notifications-node'; -import { AuthenticationError, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { AuthService, DiscoveryService, HttpAuthService, LoggerService, + UserInfoService, } from '@backstage/backend-plugin-api'; import { SignalService } from '@backstage/plugin-signals-node'; import { @@ -53,15 +48,14 @@ import { /** @internal */ export interface RouterOptions { logger: LoggerService; - identity: IdentityApi; database: PluginDatabaseManager; - tokenManager: TokenManager; discovery: DiscoveryService; + auth: AuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; signalService?: SignalService; catalog?: CatalogApi; processors?: NotificationProcessor[]; - auth?: AuthService; - httpAuth?: HttpAuthService; } /** @internal */ @@ -71,7 +65,9 @@ export async function createRouter( const { logger, database, - identity, + auth, + httpAuth, + userInfo, discovery, catalog, processors, @@ -82,14 +78,10 @@ export async function createRouter( catalog ?? new CatalogClient({ discoveryApi: discovery }); const store = await DatabaseNotificationsStore.create({ database }); - const { auth, httpAuth } = createLegacyAuthAdapters(options); - const getUser = async (req: Request) => { - const user = await identity.getIdentity({ request: req }); - if (!user) { - throw new AuthenticationError(); - } - return user.identity.userEntityRef; + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + const info = await userInfo.getUserInfo(credentials); + return info.userEntityRef; }; const getUsersForEntityRef = async ( @@ -277,18 +269,16 @@ export async function createRouter( }); // Add new notification - // Allowed only for service-to-service authentication, uses `getUsersForEntityRef` to retrieve recipients for - // specific entity reference router.post('/', async (req, res) => { - const { recipients, origin, payload } = req.body; + const { recipients, payload } = req.body; const notifications = []; let users = []; - await httpAuth.credentials(req, { allow: ['service'] }); + const credentials = await httpAuth.credentials(req, { allow: ['service'] }); - const { title, link, scope } = payload; + const { title, scope } = payload; - if (!recipients || !title || !origin || !link) { + if (!recipients || !title) { logger.error(`Invalid notification request received`); throw new InputError(); } @@ -305,6 +295,7 @@ export async function createRouter( throw new InputError(); } + const origin = credentials.principal.subject; const baseNotification: Omit = { payload: { ...payload, diff --git a/plugins/notifications-backend/src/service/standaloneServer.ts b/plugins/notifications-backend/src/service/standaloneServer.ts index c4927e8b38..817c35f739 100644 --- a/plugins/notifications-backend/src/service/standaloneServer.ts +++ b/plugins/notifications-backend/src/service/standaloneServer.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { + createLegacyAuthAdapters, createServiceBuilder, HostDiscovery, loadBackendConfig, @@ -37,6 +38,11 @@ import { EventsService, EventsServiceSubscribeOptions, } from '@backstage/plugin-events-node'; +import { + AuthService, + HttpAuthService, + UserInfoService, +} from '@backstage/backend-plugin-api'; export interface ServerOptions { port: number; @@ -107,15 +113,25 @@ export async function startStandaloneServer( }; const signalService = DefaultSignalService.create({ events }); + // TODO: Move to use services instead this hack + const { auth, httpAuth, userInfo } = createLegacyAuthAdapters< + any, + { auth: AuthService; httpAuth: HttpAuthService; userInfo: UserInfoService } + >({ + identity: identityMock, + tokenManager, + discovery, + }); const router = await createRouter({ logger, - identity: identityMock, database: dbMock, catalog: catalogApi, discovery, - tokenManager, signalService, + auth, + httpAuth, + userInfo, }); let service = createServiceBuilder(module) diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index 55c9b28489..bd01aa7f5a 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -27,7 +27,7 @@ export type NotificationPayload = { title: string; description?: string; link?: string; - severity: NotificationSeverity; + severity?: NotificationSeverity; topic?: string; scope?: string; icon?: string; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index c338a772d7..2a5f803652 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -24,7 +24,7 @@ export type NotificationPayload = { link?: string; // TODO: Add support for additional links // additionalLinks?: string[]; - severity: NotificationSeverity; + severity?: NotificationSeverity; topic?: string; scope?: string; icon?: string; diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 24ae31904c..1e0e5718d1 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -51,7 +51,6 @@ export const notificationService: ServiceRef; export type NotificationServiceOptions = { auth: AuthService; discovery: DiscoveryService; - pluginId: string; }; // @public (undocumented) diff --git a/plugins/notifications-node/src/lib.ts b/plugins/notifications-node/src/lib.ts index 2ba33608b4..3e0095f72c 100644 --- a/plugins/notifications-node/src/lib.ts +++ b/plugins/notifications-node/src/lib.ts @@ -31,13 +31,11 @@ export const notificationService = createServiceRef({ deps: { auth: coreServices.auth, discovery: coreServices.discovery, - pluginMetadata: coreServices.pluginMetadata, }, - factory({ auth, discovery, pluginMetadata }) { + factory({ auth, discovery }) { return DefaultNotificationService.create({ auth, discovery, - pluginId: pluginMetadata.getId(), }); }, }), diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts index 9d8f5b82d9..4711f3de18 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts @@ -42,7 +42,6 @@ describe('DefaultNotificationService', () => { service = DefaultNotificationService.create({ auth, discovery, - pluginId: 'test', }); }); @@ -58,7 +57,7 @@ describe('DefaultNotificationService', () => { `${await discovery.getBaseUrl('notifications')}/`, async (req, res, ctx) => { const json = await req.json(); - expect(json).toEqual({ ...body, origin: 'plugin-test' }); + expect(json).toEqual(body); expect(req.headers.get('Authorization')).toBe( mockCredentials.service.header({ onBehalfOf: await auth.getOwnServiceCredentials(), @@ -83,7 +82,7 @@ describe('DefaultNotificationService', () => { `${await discovery.getBaseUrl('notifications')}/`, async (req, res, ctx) => { const json = await req.json(); - expect(json).toEqual({ ...body, origin: 'plugin-test' }); + expect(json).toEqual(body); expect(req.headers.get('Authorization')).toBe( mockCredentials.service.header({ onBehalfOf: await auth.getOwnServiceCredentials(), diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.ts b/plugins/notifications-node/src/service/DefaultNotificationService.ts index ef06133d36..bd7712ff6f 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.ts @@ -22,7 +22,6 @@ import { NotificationPayload } from '@backstage/plugin-notifications-common'; export type NotificationServiceOptions = { auth: AuthService; discovery: DiscoveryService; - pluginId: string; }; /** @public */ @@ -45,17 +44,12 @@ export class DefaultNotificationService implements NotificationService { private constructor( private readonly discovery: DiscoveryService, private readonly auth: AuthService, - private readonly pluginId: string, ) {} static create( options: NotificationServiceOptions, ): DefaultNotificationService { - return new DefaultNotificationService( - options.discovery, - options.auth, - options.pluginId, - ); + return new DefaultNotificationService(options.discovery, options.auth); } async send(notification: NotificationSendOptions): Promise { @@ -65,13 +59,10 @@ export class DefaultNotificationService implements NotificationService { onBehalfOf: await this.auth.getOwnServiceCredentials(), targetPluginId: 'notifications', }); + const response = await fetch(`${baseUrl}/`, { method: 'POST', - body: JSON.stringify({ - ...notification, - // TODO: Should retrieve this in the backend from service auth instead - origin: `plugin-${this.pluginId}`, - }), + body: JSON.stringify(notification), headers: { 'Content-Type': 'application/json', Accept: 'application/json', From db8358d6b15ead5b2ecd10a8a99dbb6ac92923da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 14:46:19 +0100 Subject: [PATCH 458/483] config-loader: forward null values Signed-off-by: Patrik Oldsberg --- .changeset/gorgeous-apples-reply.md | 5 +++++ .../config-loader/src/sources/transform/apply.test.ts | 8 +++++--- packages/config-loader/src/sources/transform/apply.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/gorgeous-apples-reply.md diff --git a/.changeset/gorgeous-apples-reply.md b/.changeset/gorgeous-apples-reply.md new file mode 100644 index 0000000000..c781617d22 --- /dev/null +++ b/.changeset/gorgeous-apples-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Forward `null` values read from configuration files in configuration data, rather treating them as absence of config. diff --git a/packages/config-loader/src/sources/transform/apply.test.ts b/packages/config-loader/src/sources/transform/apply.test.ts index ea87734036..9f1dfbc672 100644 --- a/packages/config-loader/src/sources/transform/apply.test.ts +++ b/packages/config-loader/src/sources/transform/apply.test.ts @@ -17,7 +17,7 @@ import { applyConfigTransforms } from './apply'; describe('applyConfigTransforms', () => { - it('should apply not transforms to input', async () => { + it('should apply no transforms to input', async () => { const data = applyConfigTransforms( { app: { @@ -35,7 +35,8 @@ describe('applyConfigTransforms', () => { app: { title: 'Test', x: 1, - y: [true], + y: [null, true], + z: null, }, }); }); @@ -77,7 +78,8 @@ describe('applyConfigTransforms', () => { app: { title: ['T', 'e', 's', 't'], x: 2, - y: [true], + y: [null, true], + z: null, }, }); }); diff --git a/packages/config-loader/src/sources/transform/apply.ts b/packages/config-loader/src/sources/transform/apply.ts index d0d8650fe7..48d9c5a1e8 100644 --- a/packages/config-loader/src/sources/transform/apply.ts +++ b/packages/config-loader/src/sources/transform/apply.ts @@ -58,7 +58,7 @@ export async function applyConfigTransforms( if (typeof obj !== 'object') { return obj; } else if (obj === null) { - return undefined; + return null; } else if (Array.isArray(obj)) { const arr = new Array(); From 27c14973558c5b87a4c9d8dc23bd640dda29f84b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 21:28:22 +0100 Subject: [PATCH 459/483] docs/tutorials/auth-service-migration: add guest auth instructions Signed-off-by: Patrik Oldsberg --- docs/tutorials/auth-service-migration.md | 33 +++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/auth-service-migration.md b/docs/tutorials/auth-service-migration.md index 52c47e0ed0..83d8514e7d 100644 --- a/docs/tutorials/auth-service-migration.md +++ b/docs/tutorials/auth-service-migration.md @@ -28,7 +28,38 @@ In short, this will allow requests through to plugins in your backend, even if t ### Migrating the backend -If you do want to keep the default auth policy in effect, there is little action needed to migrate your backend. Be sure to upgrade all plugins to their latest versions to pick up any updates that may be needed for the new auth services. If you have any internal plugins or modules, refer to the plugin migration section below. +If you do want to keep the default auth policy in effect, there is little action needed to migrate the backend itself. Be sure to upgrade all plugins to their latest versions to pick up any updates that may be needed for the new auth services. If you have any internal plugins or modules, refer to the plugin migration section below. + +With the default auth policy in effect you will now need to ensure that the requests to your backend are authenticated, also during local development. If you already have a setup where you use an auth provider for local development, you can keep using that. But, if you rely on the `'guest'` access for local development we recommend that you install the new guest provider module in your auth backend: + +```sh +yarn install @backstage/plugin-auth-backend-module-guest-provider +``` + +Add it to your backend: + +```ts title="packages/backend/src/index.ts" +// highlight-add-next-line +backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +``` + +Lastly, add the following to your development configuration: + +```yaml +auth: + providers: + guest: {} +``` + +Make sure that you only enable the guest provider for local development, and not in production. It will refuse to be enabled in production by default, but you it still best to avoid it entirely. If you do not have a separate development configuration, add the following to your production configuration: + +```yaml +auth: + providers: + guest: null +``` + +That's all you need for guest authentication! The default `SignInPage` from `@backstage/core-components` will detect and use the guest provider if it's enabled. Since the default auth policy is in effect for all plugins running in the new backend system, you do not need to worry about whether individual plugins are protected or not. The impact of plugins not yet being migrated is that they may have endpoints that should allow unauthenticated requests, but are now blocked by the default auth policy. If you want to temporarily work around this for individual plugins, you can install a module for the plugin that adds the required policy via the [http router service](../backend-system/core-services/http-router.md). From 50cf9df43c3c3a9b67f9ade14e0603ed84fffe95 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 19:17:25 +0100 Subject: [PATCH 460/483] config: treat null as explicitly undefined Signed-off-by: Patrik Oldsberg --- .changeset/clever-donkeys-fry.md | 5 ++ packages/config/package.json | 3 +- packages/config/src/reader.test.ts | 53 ++++++++++++++++-- packages/config/src/reader.ts | 87 ++++++++++++++++++++++-------- yarn.lock | 1 - 5 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 .changeset/clever-donkeys-fry.md diff --git a/.changeset/clever-donkeys-fry.md b/.changeset/clever-donkeys-fry.md new file mode 100644 index 0000000000..8098bba682 --- /dev/null +++ b/.changeset/clever-donkeys-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/config': minor +--- + +The `ConfigReader` now treats `null` values as present but explicitly undefined, meaning it will not fall back to the next level of configuration. diff --git a/packages/config/package.json b/packages/config/package.json index ddfe2da633..d8a2df01c5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -37,8 +37,7 @@ }, "dependencies": { "@backstage/errors": "workspace:^", - "@backstage/types": "workspace:^", - "lodash": "^4.17.21" + "@backstage/types": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 90b9b28b41..97982ac265 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -48,15 +48,15 @@ const DATA = { }; function expectValidValues(config: ConfigReader) { - expect(config.keys()).toEqual(Object.keys(DATA)); + expect(config.keys()).toEqual(Object.keys(DATA).filter(k => k !== 'null')); expect(config.get('zero')).toBe(0); expect(config.has('zero')).toBe(true); expect(config.has('false')).toBe(true); - expect(config.has('null')).toBe(true); + expect(config.has('null')).toBe(false); expect(config.has('missing')).toBe(false); expect(config.has('nested.one')).toBe(true); expect(config.has('nested.missing')).toBe(false); - expect(config.has('nested.null')).toBe(true); + expect(config.has('nested.null')).toBe(false); expect(config.getNumber('zero')).toBe(0); expect(config.getNumber('one')).toBe(1); expect(config.getNumber('zeroString')).toBe(0); @@ -81,7 +81,7 @@ function expectValidValues(config: ConfigReader) { expect(config.getConfig('nested').getNumber('one')).toBe(1); expect(config.get('nested')).toEqual({ one: 1, - null: null, + null: undefined, string: 'string', strings: ['string1', 'string2'], }); @@ -90,6 +90,8 @@ function expectValidValues(config: ConfigReader) { ['string1', 'string2'], ); expect(config.getOptional('missing')).toBe(undefined); + expect(config.getOptionalConfig('null')).toBe(undefined); + expect(config.getOptionalConfig('null.nested')).toBe(undefined); expect(config.getOptionalConfig('missing')).toBe(undefined); expect(config.getOptionalConfigArray('missing')).toBe(undefined); expect(config.getNumber('zero')).toBe(0); @@ -120,7 +122,7 @@ function expectInvalidValues(config: ConfigReader) { "Invalid type in config for key 'true' in 'ctx', got boolean, wanted number", ); expect(() => config.getStringArray('null')).toThrow( - "Invalid type in config for key 'null' in 'ctx', got null, wanted string-array", + "Missing required config value at 'null' in 'ctx'", ); expect(() => config.getString('emptyString')).toThrow( "Invalid type in config for key 'emptyString' in 'ctx', got empty-string, wanted string", @@ -137,6 +139,9 @@ function expectInvalidValues(config: ConfigReader) { expect(() => config.getConfig('one')).toThrow( "Invalid type in config for key 'one' in 'ctx', got number, wanted object", ); + expect(() => config.getConfig('null')).toThrow( + "Missing required config value at 'null'", + ); expect(() => config.getConfigArray('one')).toThrow( "Invalid type in config for key 'one' in 'ctx', got number, wanted object-array", ); @@ -742,4 +747,42 @@ describe('ConfigReader.get()', () => { expect(data1.foo.baz).toEqual({}); expect(data2.x.y.z).toEqual({}); }); + + it('should treat null as explicitly undefined', () => { + const reader = ConfigReader.fromConfigs([ + { + data: { obj: { a: 2, b: 2, c: 2 }, objB: { a: 2, b: null } }, + context: 'fallback', + }, + { + data: { obj: { a: 1, b: null }, objA: { a: 1, b: null } }, + context: 'primary', + }, + ]); + + expect(reader.getOptionalNumber('obj.a')).toBe(1); + expect(reader.getOptionalNumber('obj.b')).toBe(undefined); + expect(reader.getOptionalNumber('obj.c')).toBe(2); + + expect(reader.getConfig('obj').getOptionalNumber('a')).toBe(1); + expect(reader.getConfig('obj').getOptionalNumber('b')).toBe(undefined); + expect(reader.getConfig('obj').getOptionalNumber('c')).toBe(2); + + expect(reader.getConfig('obj').get('a')).toBe(1); + expect(() => reader.getConfig('obj').get('b')).toThrow( + "Missing required config value at 'obj.b' in 'primary'", + ); + expect(reader.getConfig('obj').get('c')).toBe(2); + + expect(reader.getConfig('obj').getOptional('a')).toBe(1); + expect(reader.getConfig('obj').getOptional('b')).toBe(undefined); + expect(reader.getConfig('obj').getOptional('c')).toBe(2); + + expect(reader.get('obj')).toEqual({ a: 1, c: 2 }); + expect(reader.getConfig('obj').get()).toEqual({ a: 1, c: 2 }); + expect(reader.getConfig('obj').keys()).toEqual(['a', 'c']); + + expect(reader.get('objA')).toEqual({ a: 1 }); + expect(reader.get('objB')).toEqual({ a: 2 }); + }); }); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 4a570b6977..52c33f8618 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -16,8 +16,6 @@ import { JsonValue, JsonObject } from '@backstage/types'; import { AppConfig, Config } from './types'; -import cloneDeep from 'lodash/cloneDeep'; -import mergeWith from 'lodash/mergeWith'; // Update the same pattern in config-loader package if this is changed const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; @@ -26,6 +24,43 @@ function isObject(value: JsonValue | undefined): value is JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function cloneDeep(value: JsonValue | null | undefined): JsonValue | undefined { + if (typeof value !== 'object' || value === null) { + return value; + } + if (Array.isArray(value)) { + return value.map(cloneDeep) as JsonValue; + } + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]), + ); +} + +function merge( + into: JsonValue | undefined, + from?: JsonValue | undefined, +): JsonValue | undefined { + if (into === null) { + return undefined; + } + if (into === undefined) { + return from === undefined ? undefined : merge(from); + } + if (typeof into !== 'object' || Array.isArray(into)) { + return into; + } + const fromObj = isObject(from) ? from : {}; + + const out: JsonObject = {}; + for (const key of new Set([...Object.keys(into), ...Object.keys(fromObj)])) { + const val = merge(into[key], fromObj[key]); + if (val !== undefined) { + out[key] = val; + } + } + return out; +} + function typeOf(value: JsonValue | undefined): string { if (value === null) { return 'null'; @@ -47,8 +82,8 @@ const errors = { type(key: string, context: string, typeName: string, expected: string) { return `Invalid type in config for key '${key}' in '${context}', got ${typeName}, wanted ${expected}`; }, - missing(key: string) { - return `Missing required config value at '${key}'`; + missing(key: string, context: string) { + return `Missing required config value at '${key}' in '${context}'`; }, convert(key: string, context: string, expected: string) { return `Unable to convert config value for key '${key}' in '${context}' to a ${expected}`; @@ -114,6 +149,9 @@ export class ConfigReader implements Config { /** {@inheritdoc Config.has} */ has(key: string): boolean { const value = this.readValue(key); + if (value === null) { + return false; + } if (value !== undefined) { return true; } @@ -124,14 +162,16 @@ export class ConfigReader implements Config { keys(): string[] { const localKeys = this.data ? Object.keys(this.data) : []; const fallbackKeys = this.fallback?.keys() ?? []; - return [...new Set([...localKeys, ...fallbackKeys])]; + return [...new Set([...localKeys, ...fallbackKeys])].filter( + k => this.data?.[k] !== null, + ); } /** {@inheritdoc Config.get} */ get(key?: string): T { const value = this.getOptional(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key ?? ''))); + throw new Error(errors.missing(this.fullKey(key ?? ''), this.context)); } return value as T; } @@ -139,8 +179,11 @@ export class ConfigReader implements Config { /** {@inheritdoc Config.getOptional} */ getOptional(key?: string): T | undefined { const value = cloneDeep(this.readValue(key)); - const fallbackValue = this.fallback?.getOptional(key); + const fallbackValue = this.fallback?.getOptional(key); + if (value === null) { + return undefined; + } if (value === undefined) { if (process.env.NODE_ENV === 'development') { if (fallbackValue === undefined && key) { @@ -158,23 +201,19 @@ export class ConfigReader implements Config { } } } - return fallbackValue; + return merge(fallbackValue) as T; } else if (fallbackValue === undefined) { - return value as T; + return merge(value) as T; } - // Avoid merging arrays and primitive values, since that's how merging works for other - // methods for reading config. - return mergeWith({}, { value: fallbackValue }, { value }, (into, from) => - !isObject(from) || !isObject(into) ? from : undefined, - ).value as T; + return merge(value, fallbackValue) as T; } /** {@inheritdoc Config.getConfig} */ getConfig(key: string): ConfigReader { const value = this.getOptionalConfig(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -187,6 +226,9 @@ export class ConfigReader implements Config { if (isObject(value)) { return this.copy(value, key, fallbackConfig); } + if (value === null) { + return undefined; + } if (value !== undefined) { throw new TypeError( errors.type(this.fullKey(key), this.context, typeOf(value), 'object'), @@ -199,7 +241,7 @@ export class ConfigReader implements Config { getConfigArray(key: string): ConfigReader[] { const value = this.getOptionalConfigArray(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -244,7 +286,7 @@ export class ConfigReader implements Config { getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -273,7 +315,7 @@ export class ConfigReader implements Config { getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -305,7 +347,7 @@ export class ConfigReader implements Config { getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -323,7 +365,7 @@ export class ConfigReader implements Config { getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { - throw new Error(errors.missing(this.fullKey(key))); + throw new Error(errors.missing(this.fullKey(key), this.context)); } return value; } @@ -384,6 +426,9 @@ export class ConfigReader implements Config { return this.fallback?.readConfigValue(key, validate); } + if (value === null) { + return undefined; + } const result = validate(value); if (result !== true) { const { key: keyName = key, value: theValue = value, expected } = result; @@ -416,7 +461,7 @@ export class ConfigReader implements Config { for (const [index, part] of parts.entries()) { if (isObject(value)) { value = value[part]; - } else if (value !== undefined) { + } else if (value !== undefined && value !== null) { const badKey = this.fullKey(parts.slice(0, index).join('.')); throw new TypeError( errors.type(badKey, this.context, typeOf(value), 'object'), diff --git a/yarn.lock b/yarn.lock index 1693b3c283..fafc485f01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3803,7 +3803,6 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" - lodash: ^4.17.21 languageName: unknown linkType: soft From b017906f7acb11e52b3c8275c3452bd5e9089b4a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 21:14:25 +0100 Subject: [PATCH 461/483] docs/config/writing: document new null behavior Signed-off-by: Patrik Oldsberg --- docs/conf/writing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf/writing.md b/docs/conf/writing.md index e0eb72f828..48104699c7 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -101,6 +101,9 @@ All loaded configuration files are merged together using the following rules: contents. - Objects are merged together deeply, meaning that if any of the included configs contain a value for a given path, it will be found. +- A `null` value in a config file will be treated as an explicit absence of + configuration. This means that the reading will not fall back to a lower priority + config, but it will still be treated as if the configuration was not present. The priority of the configurations is determined by the following rules, in order: From 2ff3e6e8a6ffe744f8b299bd5a684288e2ae95cc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 12:21:08 +0100 Subject: [PATCH 462/483] core-components: roll back translation of Link component Signed-off-by: Patrik Oldsberg --- .changeset/curvy-carrots-dream.md | 5 +++++ packages/core-components/api-report-alpha.md | 1 - packages/core-components/src/components/Link/Link.tsx | 5 +---- packages/core-components/src/translation.ts | 3 --- 4 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 .changeset/curvy-carrots-dream.md diff --git a/.changeset/curvy-carrots-dream.md b/.changeset/curvy-carrots-dream.md new file mode 100644 index 0000000000..64f1dfffb0 --- /dev/null +++ b/.changeset/curvy-carrots-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +The translation support for the `Link` component has been removed for now, in order to avoid broad breakages of tests in existing projects where the component is tested without being wrapped in an API provider. diff --git a/packages/core-components/api-report-alpha.md b/packages/core-components/api-report-alpha.md index f2b21f6d1e..1f4b999232 100644 --- a/packages/core-components/api-report-alpha.md +++ b/packages/core-components/api-report-alpha.md @@ -9,7 +9,6 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; export const coreComponentsTranslationRef: TranslationRef< 'core-components', { - readonly 'link.openNewWindow': 'Opens in a new window'; readonly 'table.filter.title': 'Filters'; readonly 'table.filter.clearAll': 'Clear all'; readonly 'signIn.title': 'Sign In'; diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index b426402ba9..e957390da8 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; // eslint-disable-next-line no-restricted-imports import MaterialLink, { LinkProps as MaterialLinkProps, @@ -30,7 +29,6 @@ import { LinkProps as RouterLinkProps, Route, } from 'react-router-dom'; -import { coreComponentsTranslationRef } from '../../translation'; export function isReactRouterBeta(): boolean { const [obj] = createRoutesFromChildren(} />); @@ -163,7 +161,6 @@ export const Link = React.forwardRef( ({ onClick, noTrack, ...props }, ref) => { const classes = useStyles(); const analytics = useAnalytics(); - const { t } = useTranslationRef(coreComponentsTranslationRef); // Adding the base path to URLs breaks react-router v6 stable, so we only // do it for beta. The react router version won't change at runtime so it is @@ -202,7 +199,7 @@ export const Link = React.forwardRef( > {props.children} - {`, ${t('link.openNewWindow')}`} + , Opens in a new window ) : ( diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index 2acb4ea735..91474e73b7 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -80,9 +80,6 @@ export const coreComponentsTranslationRef = createTranslationRef({ login: 'Log in', rejectAll: 'Reject All', }, - link: { - openNewWindow: 'Opens in a new window', - }, supportButton: { title: 'Support', close: 'Close', From 3e43849316964210a46d15f6926ea2623fdab383 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 22:47:03 +0100 Subject: [PATCH 463/483] microsite: update RadHat link to point to RHDP Signed-off-by: Patrik Oldsberg --- microsite/src/pages/community/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 42c42ae0c3..d55f6bb621 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -73,7 +73,7 @@ const Community = () => { }, { name: 'RedHat', - url: 'https://www.redhat.com/', + url: 'https://developers.redhat.com/rhdh', logo: 'img/partner-logo-redhat.png', }, { From 3044624ff77be63ab814cdb1be61d3521ba316db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 22:47:49 +0100 Subject: [PATCH 464/483] microsite: update Thoughtworks link to point to Backstage landing page Signed-off-by: Patrik Oldsberg --- microsite/src/pages/community/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 42c42ae0c3..73c38ea637 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -83,7 +83,7 @@ const Community = () => { }, { name: 'ThoughtWorks', - url: 'https://www.thoughtworks.com', + url: 'https://www.thoughtworks.com/about-us/partnerships/technology/backstage-by-spotify', logo: 'img/partner-logo-thoughtworks.png', }, { From 0f9d1f520daa65af386bf831da1dc8b8d30ab96b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Feb 2024 22:48:44 +0100 Subject: [PATCH 465/483] microsite: update VMWare link to point to Tanzu Developer Portal Signed-off-by: Patrik Oldsberg --- microsite/src/pages/community/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index 42c42ae0c3..9cc06430b2 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -88,7 +88,7 @@ const Community = () => { }, { name: 'VMWare', - url: 'https://tanzu.vmware.com/', + url: 'https://tanzu.vmware.com/developer-portal', logo: 'img/partner-logo-tanzubybroadcom.png', }, ]; From 941dbb755083269da3183dc4208041337caa3a45 Mon Sep 17 00:00:00 2001 From: bnechyporenko Date: Thu, 29 Feb 2024 23:36:27 +0100 Subject: [PATCH 466/483] Match lighthouse accessibility threashold with an actual value Signed-off-by: bnechyporenko --- lighthouserc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouserc.js b/lighthouserc.js index 70136ee158..5e21730b7d 100644 --- a/lighthouserc.js +++ b/lighthouserc.js @@ -64,7 +64,7 @@ module.exports = { 'categories:pwa': 'off', 'categories:best-practices': 'off', 'categories:seo': 'off', - 'categories:accessibility': ['error', { minScore: 0.95 }], + 'categories:accessibility': ['error', { minScore: 0.89 }], }, }, }, From 023bb35c98c90fe7661761fbcefef2839af64478 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 00:06:41 +0100 Subject: [PATCH 467/483] Update .changeset/gorgeous-apples-reply.md Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Signed-off-by: Patrik Oldsberg --- .changeset/gorgeous-apples-reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/gorgeous-apples-reply.md b/.changeset/gorgeous-apples-reply.md index c781617d22..a6bf04d0fe 100644 --- a/.changeset/gorgeous-apples-reply.md +++ b/.changeset/gorgeous-apples-reply.md @@ -2,4 +2,4 @@ '@backstage/config-loader': minor --- -Forward `null` values read from configuration files in configuration data, rather treating them as absence of config. +Forward `null` values read from configuration files in configuration data, rather than treating them as an absence of config. From 3ca11484aa78d268ac2bb39d5036d62ad7de2b09 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 09:50:06 +0100 Subject: [PATCH 468/483] Update docs/tutorials/auth-service-migration.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: Patrik Oldsberg --- docs/tutorials/auth-service-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/auth-service-migration.md b/docs/tutorials/auth-service-migration.md index 83d8514e7d..8033350c37 100644 --- a/docs/tutorials/auth-service-migration.md +++ b/docs/tutorials/auth-service-migration.md @@ -33,7 +33,7 @@ If you do want to keep the default auth policy in effect, there is little action With the default auth policy in effect you will now need to ensure that the requests to your backend are authenticated, also during local development. If you already have a setup where you use an auth provider for local development, you can keep using that. But, if you rely on the `'guest'` access for local development we recommend that you install the new guest provider module in your auth backend: ```sh -yarn install @backstage/plugin-auth-backend-module-guest-provider +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider ``` Add it to your backend: From 6ab5f5f5109c3055879603b56eb443f3df84d158 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 09:50:58 +0100 Subject: [PATCH 469/483] 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: Patrik Oldsberg --- docs/tutorials/auth-service-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/auth-service-migration.md b/docs/tutorials/auth-service-migration.md index 8033350c37..c3661d1f61 100644 --- a/docs/tutorials/auth-service-migration.md +++ b/docs/tutorials/auth-service-migration.md @@ -28,7 +28,7 @@ In short, this will allow requests through to plugins in your backend, even if t ### Migrating the backend -If you do want to keep the default auth policy in effect, there is little action needed to migrate the backend itself. Be sure to upgrade all plugins to their latest versions to pick up any updates that may be needed for the new auth services. If you have any internal plugins or modules, refer to the plugin migration section below. +If you do want to keep the default auth policy in effect, there is some minor action needed to migrate the backend itself. Be sure to upgrade all plugins to their latest versions to pick up any updates that may be needed for the new auth services. If you have any internal plugins or modules, refer to the plugin migration section below. With the default auth policy in effect you will now need to ensure that the requests to your backend are authenticated, also during local development. If you already have a setup where you use an auth provider for local development, you can keep using that. But, if you rely on the `'guest'` access for local development we recommend that you install the new guest provider module in your auth backend: @@ -51,7 +51,7 @@ auth: guest: {} ``` -Make sure that you only enable the guest provider for local development, and not in production. It will refuse to be enabled in production by default, but you it still best to avoid it entirely. If you do not have a separate development configuration, add the following to your production configuration: +Make sure that you only enable the guest provider for local development, and not in production. It will refuse to be enabled in production by default, but it is still best to avoid it entirely. If you do not have a separate development configuration, add the following to your production configuration: ```yaml auth: From 6f5388b4f65b38fc5336106d3faffbc44a9a4c4d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 10:34:52 +0100 Subject: [PATCH 470/483] docs,config: fix proxy provider config wonk Signed-off-by: Patrik Oldsberg --- app-config.yaml | 6 ++---- docs/auth/guest/provider.md | 6 +----- docs/auth/microsoft/azure-easyauth.md | 3 +-- docs/auth/oauth2-proxy/provider.md | 3 +-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 4b92687653..7950768350 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -397,10 +397,8 @@ auth: clientId: ${AUTH_ATLASSIAN_CLIENT_ID} clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET} scopes: ${AUTH_ATLASSIAN_SCOPES} - myproxy: - development: {} - guest: - development: {} + myproxy: {} + guest: {} costInsights: engineerCost: 200000 diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index c730877a47..292b423199 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -58,9 +58,5 @@ Similar to the other authentication providers, you have to enable the provider i ```diff auth: providers: -+ guest: -+ userEntityRef: user:default/guest -+ development: {} ++ guest: {} ``` - -We need to specify that the provider is enabled for the given environment, and as there are no config values for this provider yet, you can just specify an empty object. diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md index 07a3923da1..733e8f62d0 100644 --- a/docs/auth/microsoft/azure-easyauth.md +++ b/docs/auth/microsoft/azure-easyauth.md @@ -15,8 +15,7 @@ Add the following into your `app-config.yaml` or `app-config.production.yaml` fi auth: environment: development providers: - azure-easyauth: - development: {} + azure-easyauth: {} ``` Add a `providerFactories` entry to the router in diff --git a/docs/auth/oauth2-proxy/provider.md b/docs/auth/oauth2-proxy/provider.md index 5cff655c02..ef11827039 100644 --- a/docs/auth/oauth2-proxy/provider.md +++ b/docs/auth/oauth2-proxy/provider.md @@ -22,8 +22,7 @@ The provider configuration can be added to your `app-config.yaml` under the root auth: environment: development providers: - oauth2Proxy: - development: {} + oauth2Proxy: {} ``` Right now no configuration options are supported, but the empty object is needed From 72dd380d142aa53f0b68813fc420aca9e574dfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 1 Mar 2024 10:21:28 +0100 Subject: [PATCH 471/483] ensure that the guest auth config schema is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/kind-shrimps-yell.md | 5 ++++ docs/auth/guest/provider.md | 26 +++++++++------- .../package.json | 30 ++++++++++--------- 3 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 .changeset/kind-shrimps-yell.md diff --git a/.changeset/kind-shrimps-yell.md b/.changeset/kind-shrimps-yell.md new file mode 100644 index 0000000000..b2ca801c17 --- /dev/null +++ b/.changeset/kind-shrimps-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-guest-provider': patch +--- + +Ensure that the config schema is present diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index 292b423199..b28274d2ba 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -25,17 +25,19 @@ This will only work with the new backend system. There is no support for this in Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. -``` +```sh +# From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider ``` -Then, add it to your backend's `index.ts` file, +Then, add it to your backend's source, -```diff +```ts title="packages/backend/src/index.ts" const backend = createBackend(); -backend.add('@backstage/plugin-auth-backend'); -+backend.add('@backstage/plugin-auth-backend-module-guest-provider'); +backend.add(import('@backstage/plugin-auth-backend')); +// highlight-add-next-line +backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); await backend.start(); ``` @@ -44,10 +46,11 @@ await backend.start(); Add the following to your `SignInPage` providers, -```diff +```ts const providers = [ -+ 'guest', - ... + // highlight-add-next-line + 'guest', + ... ] ``` @@ -55,8 +58,9 @@ const providers = [ Similar to the other authentication providers, you have to enable the provider in config. Add the following to your `app-config.local.yaml`, -```diff +```yaml title="app-config.local.yaml" auth: - providers: -+ guest: {} + providers: + # highlight-add-next-line + guest: {} ``` diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 46ef0d96d7..478d041724 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,10 +1,10 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "description": "The guest-provider backend module for the auth plugin.", "version": "0.1.0-next.0", - "main": "src/index.ts", - "types": "src/index.ts", - "license": "Apache-2.0", + "description": "The guest-provider backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module" + }, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -15,17 +15,21 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/auth-backend-module-guest-provider" }, - "backstage": { - "role": "backend-plugin-module" - }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], "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/backend-common": "workspace:^", @@ -41,7 +45,5 @@ "@backstage/config": "workspace:^", "express": "^4.18.2" }, - "files": [ - "dist" - ] + "configSchema": "config.d.ts" } From e82e31a159bc56e9120bfd586156785a60f4ac1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 12:27:55 +0000 Subject: [PATCH 472/483] chore(deps): update actions/cache digest to ab5e6d0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/uffizzi-preview.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index 9db724fa96..2377fc01dc 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -140,7 +140,7 @@ jobs: - name: Fetch cached Manifests File id: cache - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4 with: path: manifests.rendered.yml key: ${{ needs.cache-manifests-file.outputs.manifests-cache-key }} From 81e012067856b60559aae2b30496f350d525d63e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 14:07:55 +0100 Subject: [PATCH 473/483] backend-app-api: fix config secret redactions Signed-off-by: Patrik Oldsberg --- .changeset/large-candles-sniff.md | 5 ++ .../backend-app-api/src/config/config.test.ts | 74 +++++++++++++++++++ packages/backend-app-api/src/config/config.ts | 4 +- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .changeset/large-candles-sniff.md create mode 100644 packages/backend-app-api/src/config/config.test.ts diff --git a/.changeset/large-candles-sniff.md b/.changeset/large-candles-sniff.md new file mode 100644 index 0000000000..3b62d212ba --- /dev/null +++ b/.changeset/large-candles-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Fixed an issue where configuration schema for the purpose of redacting secrets from logs was not being read correctly. diff --git a/packages/backend-app-api/src/config/config.test.ts b/packages/backend-app-api/src/config/config.test.ts new file mode 100644 index 0000000000..8828c557e0 --- /dev/null +++ b/packages/backend-app-api/src/config/config.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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 { loadConfigSchema } from '@backstage/config-loader'; +import { createConfigSecretEnumerator } from './config'; +import { mockServices } from '@backstage/backend-test-utils'; + +describe('createConfigSecretEnumerator', () => { + it('should enumerate secrets', async () => { + const logger = mockServices.logger.mock(); + + const enumerate = await createConfigSecretEnumerator({ + logger, + }); + const secrets = enumerate( + mockServices.rootConfig({ + data: { + backend: { auth: { keys: [{ secret: 'my-secret-password' }] } }, + }, + }), + ); + expect(Array.from(secrets)).toEqual(['my-secret-password']); + }, 20_000); // Bit higher timeout since we're loading all config schemas in the repo + + it('should enumerate secrets with explicit schema', async () => { + const logger = mockServices.logger.mock(); + + const enumerate = await createConfigSecretEnumerator({ + logger, + schema: await loadConfigSchema({ + serialized: { + schemas: [ + { + value: { + type: 'object', + properties: { + secret: { + visibility: 'secret', + type: 'string', + }, + }, + }, + path: '/mock', + }, + ], + backstageConfigSchemaVersion: 1, + }, + }), + }); + + const secrets = enumerate( + mockServices.rootConfig({ + data: { + secret: 'my-secret', + other: 'not-secret', + }, + }), + ); + expect(Array.from(secrets)).toEqual(['my-secret']); + }); +}); diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index 8f98c5597c..60d1e46e90 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -42,7 +42,7 @@ export async function createConfigSecretEnumerator(options: { const schema = options.schema ?? (await loadConfigSchema({ - dependencies: packages.map(p => p.packageJson.name).filter(() => false), + dependencies: packages.map(p => p.packageJson.name), })); return (config: Config) => { @@ -55,7 +55,7 @@ export async function createConfigSecretEnumerator(options: { ); const secrets = new Set(); JSON.parse( - JSON.stringify(secretsData), + JSON.stringify(secretsData.data), (_, v) => typeof v === 'string' && secrets.add(v), ); logger.info( From e74185f4b25935200231ef266ee56f45e2d27521 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:11:31 +0000 Subject: [PATCH 474/483] chore(deps): update actions/cache action to v4.0.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/uffizzi-preview.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index 2377fc01dc..3125504dc8 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -76,7 +76,7 @@ jobs: - name: Cache Manifests File if: ${{ steps.event.outputs.ACTION != 'closed' }} - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: path: manifests.rendered.yml key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} From 7112a4d9e67927f9fca408da2ef332c88935030d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Mar 2024 15:03:55 +0100 Subject: [PATCH 475/483] docs/overview/versioning-policy: commit to backporting high and critical vulnerabilities Signed-off-by: Patrik Oldsberg --- docs/overview/versioning-policy.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 8b12138499..0b1bc975bd 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -68,7 +68,9 @@ The following versioning policy applies to the main-line releases only. done when necessary and with the goal of having minimal impact. When possible, there will always be a deprecation path for a breaking change. - Security fixes **may** be backported to older releases based on the simplicity - of the upgrade path, and the severity of the vulnerability. + of the upgrade path, and the severity of the vulnerability. Vulnerabilities + with a severity of `high` or `critical` will always be backported to releases + for the last 6 months if feasible. - Bug reports are valid only if reproducible in the most recent release, and bug fixes are only applied to the next release. - We will do our best to adhere to this policy. From 5c271fa51218d7bd2e22423a3d1928d0a1a21edd Mon Sep 17 00:00:00 2001 From: Harrison Hogg Date: Fri, 1 Mar 2024 16:12:49 +0100 Subject: [PATCH 476/483] Removed inline styling from breadcrumbs Signed-off-by: Harrison Hogg --- .changeset/famous-forks-listen.md | 5 +++++ packages/core-components/api-report.md | 3 +++ .../src/layout/Breadcrumbs/Breadcrumbs.tsx | 14 +++++++++++++- .../src/layout/Breadcrumbs/index.ts | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/famous-forks-listen.md diff --git a/.changeset/famous-forks-listen.md b/.changeset/famous-forks-listen.md new file mode 100644 index 0000000000..6c5aa683f1 --- /dev/null +++ b/.changeset/famous-forks-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed inline styling in breadcrumbs and replaced with a theme targetable class of BreadcrumbsCurrentPage diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 7641386176..a9372921b2 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -136,6 +136,9 @@ export function Breadcrumbs(props: Props_18): React_2.JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; +// @public (undocumented) +export type BreadcrumbsCurrentPageClassKey = 'root'; + // @public (undocumented) export type BreadcrumbsStyledBoxClassKey = 'root'; diff --git a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx index 0d11068074..49880e203b 100644 --- a/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/core-components/src/layout/Breadcrumbs/Breadcrumbs.tsx @@ -51,6 +51,18 @@ const StyledBox = withStyles( { name: 'BackstageBreadcrumbsStyledBox' }, )(Box); +/** @public */ +export type BreadcrumbsCurrentPageClassKey = 'root'; + +const BreadcrumbsCurrentPage = withStyles( + { + root: { + fontStyle: 'italic', + }, + }, + { name: 'BreadcrumbsCurrentPage' }, +)(Box); + /** * Breadcrumbs component to show navigation hierarchical structure * @@ -88,7 +100,7 @@ export function Breadcrumbs(props: Props) { {hasHiddenBreadcrumbs && ( ... )} - {currentPage} + {currentPage} Date: Thu, 29 Feb 2024 14:54:40 +0100 Subject: [PATCH 477/483] Remove -5px from SidebarScrollWrapper because it covers buttons and makes SidebarSubmenuItems hard to click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mikael Östberg --- .changeset/smart-hairs-lick.md | 5 +++++ packages/core-components/src/layout/Sidebar/Items.tsx | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/smart-hairs-lick.md diff --git a/.changeset/smart-hairs-lick.md b/.changeset/smart-hairs-lick.md new file mode 100644 index 0000000000..45b5800150 --- /dev/null +++ b/.changeset/smart-hairs-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix a spacing issue for the SidebarSubmenu in case a SidebarScrollWrapper is used that made it hard to reach the SidebarSubmenu diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index b2449075cc..cb5b801b6d 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -701,8 +701,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => { return { flex: '0 1 auto', overflowX: 'hidden', - // 5px space to the right of the scrollbar - width: 'calc(100% - 5px)', + width: '100%', // Display at least one item in the container // Question: Can this be a config/theme variable - if so, which? :/ minHeight: '48px', From 17b6bad96c793751f414f98ef3d3e4490c92f690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 1 Mar 2024 19:45:25 +0100 Subject: [PATCH 478/483] Update .changeset/empty-ladybugs-perform.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/empty-ladybugs-perform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/empty-ladybugs-perform.md b/.changeset/empty-ladybugs-perform.md index c3ed997cf8..a62c4b132b 100644 --- a/.changeset/empty-ladybugs-perform.md +++ b/.changeset/empty-ladybugs-perform.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Move the text-transform styling on BackstageTableHeader from inline styling to withStyles so it can be customised easier +Move the text-transform styling on BackstageTableHeader from inline styling to `withStyles` so it can be customised easier From 208b350de2d42cb7f68db981964f700e0283bf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 1 Mar 2024 20:26:27 +0100 Subject: [PATCH 479/483] Update .changeset/famous-forks-listen.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/famous-forks-listen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/famous-forks-listen.md b/.changeset/famous-forks-listen.md index 6c5aa683f1..784fec7411 100644 --- a/.changeset/famous-forks-listen.md +++ b/.changeset/famous-forks-listen.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Removed inline styling in breadcrumbs and replaced with a theme targetable class of BreadcrumbsCurrentPage +Removed inline styling in breadcrumbs and replaced with a theme reachable class of BreadcrumbsCurrentPage From 8ab3218c1a68a8d0e2011e59f1d50a3c57f6a162 Mon Sep 17 00:00:00 2001 From: Karthikeyan Perumal <7823084+karthikeyanjp@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:28:37 -0600 Subject: [PATCH 480/483] Fixed bug in WorkflowRunStatus component where skipped & cancelled status were shown as completed Signed-off-by: Karthikeyan Perumal <7823084+karthikeyanjp@users.noreply.github.com> --- .changeset/good-seals-argue.md | 5 +++++ .../src/components/WorkflowRunStatus/WorkflowRunStatus.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/good-seals-argue.md diff --git a/.changeset/good-seals-argue.md b/.changeset/good-seals-argue.md new file mode 100644 index 0000000000..c50d275a87 --- /dev/null +++ b/.changeset/good-seals-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': patch +--- + +Fixed bug in WorkflowRunStatus component where skipped and cancelled workflow runs appeared as success diff --git a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx index f5d1c223ba..48d736aa76 100644 --- a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx +++ b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx @@ -52,7 +52,8 @@ export function WorkflowIcon({ return ; case 'completed': switch (conclusion?.toLocaleLowerCase('en-US')) { - case 'skipped' || 'canceled': + case 'skipped': + case 'cancelled': return ; case 'timed_out': From 56fd153a37561b149c300ba03b9843d24c0f5898 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 08:43:29 +0000 Subject: [PATCH 481/483] chore(deps): update dependency @types/cookie-parser to v1.4.7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b9270f60af..3c08b1e93e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18548,11 +18548,11 @@ __metadata: linkType: hard "@types/cookie-parser@npm:^1.4.2": - version: 1.4.6 - resolution: "@types/cookie-parser@npm:1.4.6" + version: 1.4.7 + resolution: "@types/cookie-parser@npm:1.4.7" dependencies: "@types/express": "*" - checksum: b1bbb17bc4189c0e953d4996b3b58bfa20161c27db21f98353e237032e7559aec733735d8902c283300e0a4cded20e62b1a5086af608608ef30a45387e080360 + checksum: 7b87c59420598e686a57e240be6e0db53967c3c8814be9326bf86609ee2fc39c4b3b9f2263e1deba43526090121d1df88684b64c19f7b494a80a4437caf3d40b languageName: node linkType: hard From 27453fdce6dd4eaeed691f73e930b15f8d7e969e Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 2 Mar 2024 12:17:26 +0100 Subject: [PATCH 482/483] Added ternary operator for ref to docker deploy Signed-off-by: Andre Wanlin --- .github/workflows/deploy_docker-image.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 0c83a05371..9012563628 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -5,6 +5,9 @@ on: repository_dispatch: types: [release-published] +env: + RELEASE_VERSION: v${{ github.event.client_payload.version }} + jobs: build: runs-on: ubuntu-latest @@ -24,7 +27,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: backstage - ref: v${{ github.event.client_payload.version }} + ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 From dca5eb3c9df51eed76d243e8ed47aa28c945a046 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 3 Mar 2024 13:02:08 +0100 Subject: [PATCH 483/483] Docker Deploy Image tag version when available Signed-off-by: Andre Wanlin --- .github/workflows/deploy_docker-image.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 9012563628..f6cd52b284 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -7,6 +7,7 @@ on: env: RELEASE_VERSION: v${{ github.event.client_payload.version }} + TAG_VERSION: ghcr.io/${{ github.repository_owner }}/backstage:${{ github.event.client_payload.version }} jobs: build: @@ -68,6 +69,6 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | ghcr.io/${{ github.repository_owner }}/backstage:latest - ghcr.io/${{ github.repository_owner }}/backstage:${{ github.event.client_payload.version }} + ${{ github.event.client_payload.version && env.TAG_VERSION || '' }} labels: | org.opencontainers.image.description=Docker image generated from the latest Backstage release; this contains what you would get out of the box by running npx @backstage/create-app and building a Docker image from the generated source. This is meant to ease the process of evaluating Backstage for the first time, but also has the severe limitation that there is no way to install additional plugins relevant to your infrastructure.