diff --git a/.changeset/brave-ghosts-pay.md b/.changeset/brave-ghosts-pay.md new file mode 100644 index 0000000000..d8981fa53b --- /dev/null +++ b/.changeset/brave-ghosts-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-pinniped-provider': patch +--- + +Deprecated the `authModulePinnipedProvider` export. A default export is now available and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-pinniped-provider'));` diff --git a/.changeset/clean-mails-bathe.md b/.changeset/clean-mails-bathe.md new file mode 100644 index 0000000000..c9afc34f90 --- /dev/null +++ b/.changeset/clean-mails-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +New `backend-dynamic-feature-service` package, for the discovery of dynamic frontend and backend plugins (and modules) and the loading of the backend ones inside the backend application. diff --git a/.changeset/eight-rice-cross.md b/.changeset/eight-rice-cross.md new file mode 100644 index 0000000000..1628276817 --- /dev/null +++ b/.changeset/eight-rice-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Do not call fetch directly but rather use `fetchResponse` facility diff --git a/.changeset/fair-spies-rescue.md b/.changeset/fair-spies-rescue.md new file mode 100644 index 0000000000..d7f1ea5275 --- /dev/null +++ b/.changeset/fair-spies-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Ensure `teamReviewers` list is unique before calling API diff --git a/.changeset/fresh-mirrors-shake.md b/.changeset/fresh-mirrors-shake.md new file mode 100644 index 0000000000..a722182986 --- /dev/null +++ b/.changeset/fresh-mirrors-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-common': patch +--- + +Updated Template.v1beta3.schema.json, added a missing "presentation" field diff --git a/.changeset/heavy-moose-reflect.md b/.changeset/heavy-moose-reflect.md new file mode 100644 index 0000000000..011d3fc023 --- /dev/null +++ b/.changeset/heavy-moose-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Deprecated the `authModuleMicrosoftProvider` export. A default export is now available and should be used like this in your backend: `backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));` diff --git a/.changeset/ninety-impalas-knock.md b/.changeset/ninety-impalas-knock.md new file mode 100644 index 0000000000..78bd55dc1c --- /dev/null +++ b/.changeset/ninety-impalas-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-backend': patch +--- + +Update `README.md` diff --git a/.changeset/pre.json b/.changeset/pre.json index 8190034fe5..2ca69b284d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.90", diff --git a/.changeset/shaggy-coins-happen.md b/.changeset/shaggy-coins-happen.md new file mode 100644 index 0000000000..c20a8e0cb2 --- /dev/null +++ b/.changeset/shaggy-coins-happen.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +--- + +Fix errors when deleting SQS messages: + +- If zero messages were received, skip deletion to avoid `EmptyBatchRequest` error from the SQS client. +- If zero failures were returned from the SQS client during deletion, skip error logging. diff --git a/.changeset/silent-horses-raise.md b/.changeset/silent-horses-raise.md index de2671c3da..a03d662b03 100644 --- a/.changeset/silent-horses-raise.md +++ b/.changeset/silent-horses-raise.md @@ -4,4 +4,4 @@ '@backstage/frontend-app-api': patch --- -Added `elements` and `wrappers` inputs to `app/root`, that let you add things to the root of the React tree above the layout. You can use the `createAppRootElementExtension` and `createAppRootWrapperExtension` extension creator, respectively, to conveniently create such extensions. +Added `elements`, `wrappers`, and `router` inputs to `app/root`, that let you add things to the root of the React tree above the layout. You can use the `createAppRootElementExtension`, `createAppRootWrapperExtension`, and `createRouterExtension` extension creator, respectively, to conveniently create such extensions. These are all optional, and if you do not supply a router a default one will be used (`BrowserRouter` in regular runs, `MemoryRouter` in tests/CI). diff --git a/.changeset/sixty-plums-switch.md b/.changeset/sixty-plums-switch.md new file mode 100644 index 0000000000..8da07609c6 --- /dev/null +++ b/.changeset/sixty-plums-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Show first scaffolder output text by default diff --git a/.changeset/thirty-cats-help.md b/.changeset/thirty-cats-help.md new file mode 100644 index 0000000000..9c5d24180d --- /dev/null +++ b/.changeset/thirty-cats-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Parse the URL using a different method rather than `git-url-parse` to support wildcards for URLs which are not VCS providers diff --git a/.changeset/weak-pans-accept.md b/.changeset/weak-pans-accept.md new file mode 100644 index 0000000000..5e75f52058 --- /dev/null +++ b/.changeset/weak-pans-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +add oauth dialog and alert display to the root elements diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml index a9cce41fce..c1659097a7 100644 --- a/.github/workflows/uffizzi-preview.yaml +++ b/.github/workflows/uffizzi-preview.yaml @@ -12,16 +12,23 @@ jobs: name: Cache Manifests File runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + # "If you specify the access for any of these scopes, all of those that are not specified are set to none." + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions + actions: read # Access cache outputs: - manifests-cache-key: ${{ env.MANIFESTS_FILE_HASH }} - git-ref: ${{ env.GIT_REF }} - pr-number: ${{ env.PR_NUMBER }} - action: ${{ env.ACTION }} + manifests-cache-key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} + git-ref: ${{ steps.event.outputs.GIT_REF }} + pr-number: ${{ steps.event.outputs.PR_NUMBER }} + action: ${{ steps.event.outputs.ACTION }} steps: - name: Harden Runner uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 with: - egress-policy: audit + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 - name: 'Download artifacts' # Fetch output (zip archive) from the workflow run that triggered this workflow. @@ -48,59 +55,50 @@ jobs: let fs = require('fs'); fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data)); - - name: 'Unzip artifact' - run: unzip preview-spec.zip + - name: 'Accept event from first stage' + run: unzip preview-spec.zip event.json - name: Read Event into ENV + id: event run: | - echo 'EVENT_JSON<> $GITHUB_ENV - cat event.json >> $GITHUB_ENV - echo -e '\nEOF' >> $GITHUB_ENV + echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_OUTPUT + echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT + echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT - name: Hash Rendered Manifests File id: hash # If the previous workflow was triggered by a PR close event, we will not have a manifests file artifact. - if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + if: ${{ steps.event.outputs.ACTION != 'closed' }} run: | + unzip preview-spec.zip manifests.rendered.yml ls - echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV + echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT - name: Cache Manifests File - if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }} + if: ${{ steps.event.outputs.ACTION != 'closed' }} uses: actions/cache@v3.3.2 with: path: manifests.rendered.yml - key: ${{ env.MANIFESTS_FILE_HASH }} - - - name: Read PR Number From Event Object - id: pr - run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV - - - name: Read Event Type from Event Object - id: action - run: echo "ACTION=${{ fromJSON(env.EVENT_JSON).action }}" >> $GITHUB_ENV - - - name: Read Git Ref From Event Object - id: ref - run: echo "GIT_REF=${{ fromJSON(env.EVENT_JSON).pull_request.head.sha }}" >> $GITHUB_ENV + key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} - name: DEBUG - Print Job Outputs if: ${{ runner.debug }} run: | - echo "PR number: ${{ env.PR_NUMBER }}" - echo "Git Ref: ${{ env.GIT_REF }}" - echo "Manifests file hash: ${{ env.MANIFESTS_FILE_HASH }}" + echo "PR number: ${{ steps.event.outputs.PR_NUMBER }}" + echo "Git Ref: ${{ steps.event.outputs.GIT_REF }}" + echo "Action: ${{ steps.event.outputs.ACTION }}" + echo "Manifests file hash: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }}" cat event.json deploy-uffizzi-preview: - permissions: - contents: read - pull-requests: write - id-token: write name: Deploy to Uffizzi Virtual Cluster needs: - cache-manifests-file if: ${{ github.event.workflow_run.conclusion == 'success' && needs.cache-manifests-file.outputs.action != 'closed' }} + permissions: + contents: read + pull-requests: write + id-token: write runs-on: ubuntu-latest steps: - name: Checkout @@ -142,7 +140,6 @@ jobs: - name: Fetch cached Manifests File id: cache - # if: ${{ contains(fromJSON('["create", "update"]'), env.UFFIZZI_ACTION) }} uses: actions/cache@v3 with: path: manifests.rendered.yml diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 494e51320a..612d57334d 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -65,20 +65,72 @@ This is an array used to determine where to retrieve cluster configuration from. Valid cluster locator methods are: - [`catalog`](#catalog) -- [`localKubectlProxy`](#localkubectlproxy) - [`config`](#config) - [`gke`](#gke) +- [`localKubectlProxy`](#localkubectlproxy) - [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier) #### `catalog` -This cluster locator method will read cluster information from the catalog. +This cluster locator method will gather +[Resources](https://backstage.io/docs/features/software-catalog/system-model#resource) +of +[type](https://backstage.io/docs/features/software-catalog/descriptor-format#spectype-required-4) +`kubernetes-cluster` from the catalog and treat them as clusters for the +purposes of the Kubernetes plugin. In order for a resource to be detected by +this method, it must also have the following +[annotations](https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional) +(as seen +[here](https://github.com/backstage/backstage/blob/86baccb2d7d378baed74eaebf017c60b410986e5/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts#L51-L61) +in the code): -#### `localKubectlProxy` +- [`kubernetes.io/api-server`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server/), + denoting the base URL of the Kubernetes control plane +- [`kubernetes.io/api-server-certificate-authority`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server_ca/), + containing a base64-encoded certificate authority bundle in PEM format; + Backstage will check that the control plane presents a certificate signed by + this authority. +- [`kubernetes.io/auth-provider`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_auth_provider/), + denoting the strategy to use to authenticate with the control plane. -This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001). +There are many other annotations that can be applied to a cluster resource to +configure the way Backstage communicates, documented +[here](https://backstage.io/docs/reference/plugin-kubernetes-common#variables) +in the API reference. Here is a YAML snippet illustrating an example of a +cluster in the catalog: -NOTE: This cluster locator method is for local development only and should not be used in production. +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: my-cluster + annotations: + kubernetes.io/api-server: 'https://127.0.0.1:53725' + kubernetes.io/api-server-certificate-authority: # base64-encoded CA + kubernetes.io/auth-provider: 'oidc' + kubernetes.io/oidc-token-provider: 'microsoft' + kubernetes.io/skip-metrics-lookup: 'true' +spec: + type: kubernetes-cluster + owner: user:guest +``` + +Note that it is insecure to store a Kubernetes service account token in an +annotation on a catalog entity (where it could easily be accidentally revealed +by the catalog API) -- therefore there is no annotation corresponding to the +[`serviceAccountToken` field](#clustersserviceaccounttoken-optional) used by +the [`config`](#config) cluster locator. Accordingly, the catalog cluster +locator does not support the [`serviceAccount`](#clustersauthprovider) auth +strategy. + +This method can be quite helpful when used in combination with an ingestion +procedure like the +[`GkeEntityProvider`](https://backstage.io/docs/reference/plugin-catalog-backend-module-gcp.gkeentityprovider/) +(installation documented +[here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-gcp#installation)) +or the +[`AwsEKSClusterProcessor`](https://backstage.io/docs/reference/plugin-catalog-backend-module-aws.awseksclusterprocessor/) +to automatically update the set of clusters tracked by Backstage. #### `config` @@ -388,6 +440,12 @@ Defaults to `false`. Array of key value labels used to filter out clusters which don't have the matching [resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels). +#### `localKubectlProxy` + +This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001). + +NOTE: This cluster locator method is for local development only and should not be used in production. + #### Custom `KubernetesClustersSupplier` If the configuration-based cluster locators do not work for your use-case, diff --git a/microsite/data/plugins/snyk-security.yaml b/microsite/data/plugins/snyk-security.yaml index 4abd068247..8c22d44510 100644 --- a/microsite/data/plugins/snyk-security.yaml +++ b/microsite/data/plugins/snyk-security.yaml @@ -5,6 +5,6 @@ authorUrl: https://snyk.io category: Security description: View Snyk scanned vulnerabilities and license compliance of your components directly in Backstage. documentation: https://github.com/snyk-tech-services/backstage-plugin-snyk/blob/main/README.md -iconUrl: https://storage.googleapis.com/snyk-technical-services.appspot.com/snyk-logo-vertical-black.png +iconUrl: https://github.com/snyk-tech-services/backstage-plugin-snyk/assets/109112986/95eb1b06-b3e8-4910-9233-11926e02fa18 npmPackageName: 'backstage-plugin-snyk' addedDate: '2021-01-22' diff --git a/package.json b/package.json index 5fbce0ad70..4d052f2ec5 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,11 @@ }, "scripts": { "dev": "concurrently 'yarn start' 'yarn start-backend'", + "dev:next": "concurrently 'yarn start:next' 'yarn start-backend:next'", "start": "yarn workspace example-app start", "start-backend": "yarn workspace example-backend start", + "start:next": "yarn workspace example-app-next start", + "start-backend:next": "yarn workspace example-backend-next start", "build:backend": "yarn workspace example-backend build", "build:all": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 764431386d..67f6a975c1 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -105,58 +105,28 @@ export class GithubUrlReader implements UrlReader { credentials, ); - let response: Response; - try { - response = await fetch(ghUrl, { - headers: { - ...credentials?.headers, - ...(options?.etag && { 'If-None-Match': options.etag }), - ...(options?.lastModifiedAfter && { - 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), - }), - Accept: 'application/vnd.github.v3.raw', - }, - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can - // be removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - signal: options?.signal as any, - }); - } catch (e) { - throw new Error(`Unable to read ${url}, ${e}`); - } + const response = await this.fetchResponse(ghUrl, { + headers: { + ...credentials?.headers, + ...(options?.etag && { 'If-None-Match': options.etag }), + ...(options?.lastModifiedAfter && { + 'If-Modified-Since': options.lastModifiedAfter.toUTCString(), + }), + Accept: 'application/vnd.github.v3.raw', + }, + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can + // be removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + signal: options?.signal as any, + }); - if (response.status === 304) { - throw new NotModifiedError(); - } - - if (response.ok) { - return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { - etag: response.headers.get('ETag') ?? undefined, - lastModifiedAt: parseLastModified( - response.headers.get('Last-Modified'), - ), - }); - } - - let message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - throw new NotFoundError(message); - } - - // GitHub returns a 403 response with a couple of headers indicating rate - // limit status. See more in the GitHub docs: - // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting - if ( - response.status === 403 && - response.headers.get('X-RateLimit-Remaining') === '0' - ) { - message += ' (rate limit exceeded)'; - } - - throw new Error(message); + return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { + etag: response.headers.get('ETag') ?? undefined, + lastModifiedAt: parseLastModified(response.headers.get('Last-Modified')), + }); } async readTree( @@ -350,10 +320,26 @@ export class GithubUrlReader implements UrlReader { const response = await fetch(urlAsString, init); if (!response.ok) { - const message = `Request failed for ${urlAsString}, ${response.status} ${response.statusText}`; + let message = `Request failed for ${urlAsString}, ${response.status} ${response.statusText}`; + + if (response.status === 304) { + throw new NotModifiedError(); + } + if (response.status === 404) { throw new NotFoundError(message); } + + // GitHub returns a 403 response with a couple of headers indicating rate + // limit status. See more in the GitHub docs: + // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting + if ( + response.status === 403 && + response.headers.get('X-RateLimit-Remaining') === '0' + ) { + message += ' (rate limit exceeded)'; + } + throw new Error(message); } diff --git a/packages/backend-plugin-manager/.eslintrc.js b/packages/backend-dynamic-feature-service/.eslintrc.js similarity index 100% rename from packages/backend-plugin-manager/.eslintrc.js rename to packages/backend-dynamic-feature-service/.eslintrc.js diff --git a/packages/backend-plugin-manager/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md similarity index 99% rename from packages/backend-plugin-manager/CHANGELOG.md rename to packages/backend-dynamic-feature-service/CHANGELOG.md index 109c2e8409..07b118e771 100644 --- a/packages/backend-plugin-manager/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,4 +1,4 @@ -# @backstage/backend-plugin-manager +# @backstage/backend-dynamic-feature-service ## 0.0.5-next.2 diff --git a/packages/backend-plugin-manager/README.md b/packages/backend-dynamic-feature-service/README.md similarity index 64% rename from packages/backend-plugin-manager/README.md rename to packages/backend-dynamic-feature-service/README.md index c9fd666fb3..211148ee10 100644 --- a/packages/backend-plugin-manager/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -1,10 +1,6 @@ -# @backstage/backend-plugin-manager +# @backstage/backend-dynamic-feature-service -This package adds experimental support for **dynamic backend plugins**, according to the content of the proposal in [RFC 18390](https://github.com/backstage/backstage/issues/18390) - -## Status - -**This package is EXPERIMENTAL, and is subject to change according to the discussions and conclusions that happen around the RFC mentioned above.** +This package adds experimental support for **dynamic backend features (plugins and modules)**, according to the content of the proposal in [RFC 18390](https://github.com/backstage/backstage/issues/18390) ## Testing the backend dynamic plugins feature @@ -12,9 +8,9 @@ In order to test the dynamic backend plugins feature provided by this package, e ## How it works -The backend plugin manager is a service that scans a configured root directory (`dynamicPlugins.rootDirectory` in the app config) for dynamic plugin packages, and loads them dynamically. +The dynamic plugin manager is a service that scans a configured root directory (`dynamicPlugins.rootDirectory` in the app config) for dynamic plugin packages, and loads them dynamically. -In the `backend-next` application, it can be enabled by adding the `backend-plugin-manager` as a dependency in the `package.json` and the following lines in the `src/index.ts` file: +In the `backend-next` application, it can be enabled by adding the `backend-dynamic-feature-service` as a dependency in the `package.json` and the following lines in the `src/index.ts` file: ```ts const backend = createBackend(); @@ -36,9 +32,9 @@ Points 2 and 3 can be done by the `export-dynamic-plugin` CLI command used to pe ### About the `export-dynamic-plugin` command -The `export-dynamic-plugin` CLI command, used the dynamic plugin examples provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), is part of a `@backstage/cli` fork (`@dfatwork-pkgs/backstage-cli@0.22.9-next.6`), and can be used to help packaging the dynamic plugins according to the constraints mentioned above, in order to allow straightforward testing of the dynamic plugins feature. +The `export-dynamic-plugin` CLI command, used the dynamic plugin examples provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), is part of the `@janus-idp/cli` package, and can be used to help packaging the dynamic plugins according to the constraints mentioned above, in order to allow straightforward testing of the dynamic plugins feature. -However the `backend-plugin-manager` experimental package does **not** depend on the use of this additional CLI command, and in future steps of this backend dynamic plugin work, the use of such a dedicated command might not even be necessary. +However the `backend-dynamic-feature-service` experimental package does **not** depend on the use of this additional CLI command, and in future steps of this backend dynamic plugin work, the use of such a dedicated command might not even be necessary. ### About the support of the legacy backend system @@ -49,4 +45,4 @@ This is why the API related to the old backend is already marked as deprecated. ### Future work -The current implementation of the backend plugin manager is a first step towards the final implementation of the dynamic backend plugins feature, which will be completed / simplified in future steps, as the status of the backstage codebase allows it. +The current implementation of the dynamic plugin manager is a first step towards the final implementation of the dynamic features loading, which will be completed / simplified in future steps, as the status of the backstage codebase allows it. diff --git a/packages/backend-plugin-manager/api-report.md b/packages/backend-dynamic-feature-service/api-report.md similarity index 98% rename from packages/backend-plugin-manager/api-report.md rename to packages/backend-dynamic-feature-service/api-report.md index dc5973a3ab..caf2cf01ec 100644 --- a/packages/backend-plugin-manager/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/backend-plugin-manager" +## API Report File for "@backstage/backend-dynamic-feature-service" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/packages/backend-dynamic-feature-service/catalog-info.yaml b/packages/backend-dynamic-feature-service/catalog-info.yaml new file mode 100644 index 0000000000..1269a5c406 --- /dev/null +++ b/packages/backend-dynamic-feature-service/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-dynamic-feature-service + title: '@backstage/backend-dynamic-feature-service' + description: Backstage backend service to handle dynamic features +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-plugin-manager/config.d.ts b/packages/backend-dynamic-feature-service/config.d.ts similarity index 100% rename from packages/backend-plugin-manager/config.d.ts rename to packages/backend-dynamic-feature-service/config.d.ts diff --git a/packages/backend-plugin-manager/package.json b/packages/backend-dynamic-feature-service/package.json similarity index 92% rename from packages/backend-plugin-manager/package.json rename to packages/backend-dynamic-feature-service/package.json index 45344f4c94..cd95d6a5ae 100644 --- a/packages/backend-plugin-manager/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,8 +1,7 @@ { - "name": "@backstage/backend-plugin-manager", - "description": "Backstage plugin management backend", - "version": "0.0.5-next.2", - "private": true, + "name": "@backstage/backend-dynamic-feature-service", + "description": "Backstage dynamic feature service", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-plugin-manager/src/__testUtils__/testUtils.ts b/packages/backend-dynamic-feature-service/src/__testUtils__/testUtils.ts similarity index 100% rename from packages/backend-plugin-manager/src/__testUtils__/testUtils.ts rename to packages/backend-dynamic-feature-service/src/__testUtils__/testUtils.ts diff --git a/packages/backend-plugin-manager/src/index.ts b/packages/backend-dynamic-feature-service/src/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/index.ts rename to packages/backend-dynamic-feature-service/src/index.ts diff --git a/packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts rename to packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts diff --git a/packages/backend-plugin-manager/src/loader/index.ts b/packages/backend-dynamic-feature-service/src/loader/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/index.ts rename to packages/backend-dynamic-feature-service/src/loader/index.ts diff --git a/packages/backend-plugin-manager/src/loader/types.ts b/packages/backend-dynamic-feature-service/src/loader/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/loader/types.ts rename to packages/backend-dynamic-feature-service/src/loader/types.ts diff --git a/packages/backend-plugin-manager/src/manager/index.ts b/packages/backend-dynamic-feature-service/src/manager/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/index.ts rename to packages/backend-dynamic-feature-service/src/manager/index.ts diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts similarity index 99% rename from packages/backend-plugin-manager/src/manager/plugin-manager.test.ts rename to packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 405fd629f2..487729687f 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -47,7 +47,7 @@ import { PluginScanner } from '../scanner/plugin-scanner'; import { findPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; -describe('backend-plugin-manager', () => { +describe('backend-dynamic-feature-service', () => { const mockDir = createMockDirectory(); describe('loadPlugins', () => { diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/plugin-manager.ts rename to packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts diff --git a/packages/backend-plugin-manager/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/manager/types.ts rename to packages/backend-dynamic-feature-service/src/manager/types.ts diff --git a/packages/backend-plugin-manager/src/scanner/index.ts b/packages/backend-dynamic-feature-service/src/scanner/index.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/index.ts rename to packages/backend-dynamic-feature-service/src/scanner/index.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner-watcher.test.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner-watcher.test.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.test.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.test.ts diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/plugin-scanner.ts rename to packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts diff --git a/packages/backend-plugin-manager/src/scanner/types.ts b/packages/backend-dynamic-feature-service/src/scanner/types.ts similarity index 100% rename from packages/backend-plugin-manager/src/scanner/types.ts rename to packages/backend-dynamic-feature-service/src/scanner/types.ts diff --git a/packages/backend-plugin-manager/catalog-info.yaml b/packages/backend-plugin-manager/catalog-info.yaml deleted file mode 100644 index bc1845fe49..0000000000 --- a/packages/backend-plugin-manager/catalog-info.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: backstage-backend-plugin-manager - title: '@backstage/backend-plugin-manager' - description: Backstage plugin management backend -spec: - lifecycle: experimental - type: backstage-node-library - owner: maintainers diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index 883074ca00..2b709f4e42 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -17,6 +17,7 @@ import React, { ComponentType, Fragment, + PropsWithChildren, ReactNode, useContext, useState, @@ -26,6 +27,7 @@ import { createAppRootWrapperExtension, createExtension, createExtensionInput, + createRouterExtension, createSignInPageExtension, } from '@backstage/frontend-plugin-api'; import { @@ -46,6 +48,10 @@ export const AppRoot = createExtension({ name: 'root', attachTo: { id: 'app', input: 'root' }, inputs: { + router: createExtensionInput( + { component: createRouterExtension.componentDataRef }, + { singleton: true, optional: true }, + ), signInPage: createExtensionInput( { component: createSignInPageExtension.componentDataRef }, { singleton: true, optional: true }, @@ -80,7 +86,10 @@ export const AppRoot = createExtension({ return { element: ( - + {content} ), @@ -133,12 +142,18 @@ function SignInPageWrapper({ export interface AppRouterProps { children?: ReactNode; SignInPageComponent?: ComponentType; + RouterComponent?: ComponentType>; +} + +function DefaultRouter(props: PropsWithChildren<{}>) { + const configApi = useApi(configApiRef); + const basePath = getBasePath(configApi); + return {props.children}; } /** * App router and sign-in page wrapper. * - * @public * @remarks * * The AppRouter provides the routing context and renders the sign-in page. @@ -147,7 +162,11 @@ export interface AppRouterProps { * the app, while providing routing and route tracking for the app. */ export function AppRouter(props: AppRouterProps) { - const { children, SignInPageComponent } = props; + const { + children, + SignInPageComponent, + RouterComponent = DefaultRouter, + } = props; const configApi = useApi(configApiRef); const basePath = getBasePath(configApi); @@ -183,15 +202,15 @@ export function AppRouter(props: AppRouterProps) { ); return ( - + {children} - + ); } return ( - + {children} - + ); } diff --git a/packages/frontend-app-api/src/extensions/elements.tsx b/packages/frontend-app-api/src/extensions/elements.tsx new file mode 100644 index 0000000000..0a764583bb --- /dev/null +++ b/packages/frontend-app-api/src/extensions/elements.tsx @@ -0,0 +1,45 @@ +/* + * 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 { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { + createAppRootElementExtension, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +import React from 'react'; + +export const oauthRequestDialogAppRootElement = createAppRootElementExtension({ + namespace: 'app', + name: 'oauth-request-dialog', + element: , +}); + +export const alertDisplayAppRootElement = createAppRootElementExtension({ + namespace: 'app', + name: 'alert-display', + configSchema: createSchemaFromZod(z => + z.object({ + transientTimeoutMs: z.number().default(5000), + anchorOrigin: z + .object({ + vertical: z.enum(['top', 'bottom']).default('top'), + horizontal: z.enum(['left', 'center', 'right']).default('center'), + }) + .default({}), + }), + ), + element: ({ config }) => , +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index e6ddc03e38..43bb4499b1 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -259,6 +259,10 @@ describe('createApp', () => { ] ] + elements [ + + + ] ] components [ diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index b25bad03bc..7b2f17193e 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -78,6 +78,10 @@ import { apis as defaultApis } from '../../../app-defaults/src/defaults'; import { Route } from 'react-router-dom'; import { SidebarItem } from '@backstage/core-components'; import { DarkTheme, LightTheme } from '../extensions/themes'; +import { + oauthRequestDialogAppRootElement, + alertDisplayAppRootElement, +} from '../extensions/elements'; import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; import { appLanguageApiRef, @@ -116,6 +120,8 @@ export const builtinExtensions = [ DefaultNotFoundErrorPageComponent, LightTheme, DarkTheme, + oauthRequestDialogAppRootElement, + alertDisplayAppRootElement, ...DefaultApis, ].map(def => resolveExtensionDefinition(def)); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 6113c946ee..1d42d8bab8 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -687,6 +687,39 @@ export function createRouteRef< } >; +// @public +export function createRouterExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { + id: string; + input: string; + }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition; + +// @public (undocumented) +export namespace createRouterExtension { + const // (undocumented) + componentDataRef: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + {} + >; +} + // @public (undocumented) export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 4722cd1bff..007d76764b 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -22,6 +22,21 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/types": "workspace:^", + "@backstage/version-bridge": "workspace:^", + "@material-ui/core": "^4.12.4", + "@types/react": "^16.13.1 || ^17.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" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/frontend-app-api": "workspace:^", @@ -33,20 +48,5 @@ }, "files": [ "dist" - ], - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-router-dom": "6.0.0-beta.0 || ^6.3.0" - }, - "dependencies": { - "@backstage/core-components": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", - "@backstage/types": "workspace:^", - "@backstage/version-bridge": "workspace:^", - "@material-ui/core": "^4.12.4", - "@types/react": "^16.13.1 || ^17.0.0", - "lodash": "^4.17.21", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.21.4" - } + ] } diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx new file mode 100644 index 0000000000..7109e1a8aa --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { createSpecializedApp } from '@backstage/frontend-app-api'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MockConfigApi } from '@backstage/test-utils'; +import { MemoryRouter } from 'react-router-dom'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; +import { coreExtensionData } from '../wiring/coreExtensionData'; +import { createExtension } from '../wiring/createExtension'; +import { createExtensionInput } from '../wiring/createExtensionInput'; +import { createExtensionOverrides } from '../wiring/createExtensionOverrides'; +import { createPageExtension } from './createPageExtension'; +import { createRouterExtension } from './createRouterExtension'; + +describe('createRouterExtension', () => { + it('works with simple options and no props', async () => { + const extension = createRouterExtension({ + namespace: 'test', + Component: ({ children }) => ( + +
{children}
+
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-router-component', + namespace: 'test', + attachTo: { id: 'app/root', input: 'router' }, + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createPageExtension({ + namespace: 'test', + defaultPath: '/', + loader: async () =>
, + }), + ], + }), + ], + }); + + render(app.createRoot()); + + await expect( + screen.findByTestId('test-contents'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByTestId('test-router'), + ).resolves.toBeInTheDocument(); + }); + + it('works with complex options and props', async () => { + const schema = createSchemaFromZod(z => z.object({ name: z.string() })); + + const extension = createRouterExtension({ + namespace: 'test', + name: 'test', + configSchema: schema, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + Component: ({ inputs, config, children }) => ( + +
+ {children} +
+
+ ), + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + kind: 'app-router-component', + namespace: 'test', + name: 'test', + attachTo: { id: 'app/root', input: 'router' }, + configSchema: schema, + disabled: false, + inputs: { + children: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + component: expect.anything(), + }, + factory: expect.any(Function), + toString: expect.any(Function), + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: { element: coreExtensionData.reactElement }, // doesn't matter + factory: () => ({ element:
}), + }), + createPageExtension({ + namespace: 'test', + defaultPath: '/', + loader: async () =>
, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + render(app.createRoot()); + + await expect( + screen.findByTestId('test-contents'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByTestId('test-router-Robin-1'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx new file mode 100644 index 0000000000..32d61f7e60 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -0,0 +1,84 @@ +/* + * 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 React, { ComponentType, PropsWithChildren } from 'react'; +import { PortableSchema } from '../schema/types'; +import { + AnyExtensionInputMap, + ExtensionDefinition, + ResolvedExtensionInputs, + createExtension, +} from '../wiring/createExtension'; +import { createExtensionDataRef } from '../wiring/createExtensionDataRef'; +import { Expand } from '../types'; + +/** + * Creates an extension that replaces the router implementation at the app root. + * This is useful to be able to for example replace the BrowserRouter with a + * MemoryRouter in tests, or to add additional props to a BrowserRouter. + * + * @public + */ +export function createRouterExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + configSchema?: PortableSchema; + disabled?: boolean; + inputs?: TInputs; + Component: ComponentType< + PropsWithChildren<{ + inputs: Expand>; + config: TConfig; + }> + >; +}): ExtensionDefinition { + return createExtension({ + kind: 'app-router-component', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/root', input: 'router' }, + configSchema: options.configSchema, + disabled: options.disabled, + inputs: options.inputs, + output: { + component: createRouterExtension.componentDataRef, + }, + factory({ inputs, config }) { + const Component = (props: PropsWithChildren<{}>) => { + return ( + + {props.children} + + ); + }; + return { + component: Component, + }; + }, + }); +} + +/** @public */ +export namespace createRouterExtension { + export const componentDataRef = + createExtensionDataRef>>( + 'app.router.wrapper', + ); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 40e9f6f56d..562cb728cb 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -17,6 +17,7 @@ export { createApiExtension } from './createApiExtension'; export { createAppRootElementExtension } from './createAppRootElementExtension'; export { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; +export { createRouterExtension } from './createRouterExtension'; export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; export { createNavLogoExtension } from './createNavLogoExtension'; diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 40baaa3ac9..d53344f7fd 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -219,10 +219,14 @@ describe('createExtensionTester', () => { fireEvent.click(await screen.findByRole('button', { name: 'See details' })); await waitFor(() => - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'See details', - }), + expect(analyticsApiMock.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: 'click', + subject: 'See details', + }), + ]), + ), ); }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 2984af9dbf..a98879fbba 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -14,29 +14,20 @@ * limitations under the License. */ -import React, { - ComponentType, - Fragment, - ReactNode, - useContext, - useState, -} from 'react'; +import React from 'react'; import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { ExtensionDefinition, IconComponent, - IdentityApi, RouteRef, - configApiRef, coreExtensionData, - createAppRootWrapperExtension, createExtension, createExtensionInput, createExtensionOverrides, createNavItemExtension, - useApi, + createRouterExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; @@ -44,15 +35,7 @@ import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { createSignInPageExtension } from '../../../frontend-plugin-api/src/extensions/createSignInPageExtension'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/createExtension'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { InternalAppContext } from '../../../frontend-app-api/src/wiring/InternalAppContext'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { SignInPageProps } from '../../../core-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { getBasePath } from '../../../core-app-api/src/app/AppRouter'; const NavItem = (props: { routeRef: RouteRef; @@ -102,113 +85,6 @@ const TestAppNavExtension = createExtension({ }, }); -const AuthenticationProvider = (props: { - signInPage?: ComponentType; - children: ReactNode; -}) => { - const { signInPage: SignInPage, children } = props; - const configApi = useApi(configApiRef); - const signOutTargetUrl = getBasePath(configApi) || '/'; - - const internalAppContext = useContext(InternalAppContext); - if (!internalAppContext) { - throw new Error('AppRouter must be rendered within the AppProvider'); - } - - const { appIdentityProxy } = internalAppContext; - const [identityApi, setIdentityApi] = useState(); - - if (!SignInPage) { - appIdentityProxy.setTarget( - { - getUserId: () => 'guest', - getIdToken: async () => undefined, - getProfile: () => ({ - email: 'guest@example.com', - displayName: 'Guest', - }), - getProfileInfo: async () => ({ - email: 'guest@example.com', - displayName: 'Guest', - }), - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - getCredentials: async () => ({}), - signOut: async () => {}, - }, - { signOutTargetUrl }, - ); - - return children; - } - - if (!identityApi) { - return ; - } - - appIdentityProxy.setTarget(identityApi, { - signOutTargetUrl, - }); - - return children; -}; - -const TestAppRootExtension = createExtension({ - namespace: 'app', - name: 'root', - attachTo: { id: 'app', input: 'root' }, - inputs: { - signInPage: createExtensionInput( - { component: createSignInPageExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - children: createExtensionInput( - { element: coreExtensionData.reactElement }, - { singleton: true }, - ), - elements: createExtensionInput( - { element: coreExtensionData.reactElement }, - { optional: true }, - ), - wrappers: createExtensionInput( - { component: createAppRootWrapperExtension.componentDataRef }, - { optional: true }, - ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - const SignInPage = inputs.signInPage?.output.component; - - let content: React.ReactNode = ( - <> - {inputs.elements.map(el => ( - {el.output.element} - ))} - {inputs.children.output.element} - - ); - - for (const wrapper of inputs.wrappers) { - content = {content}; - } - - return { - element: ( - - - {content} - - - ), - }; - }, -}); - /** @public */ export class ExtensionTester { /** @internal */ @@ -302,7 +178,12 @@ export class ExtensionTester { extensions: [ ...this.#extensions.map(extension => extension.definition), TestAppNavExtension, - TestAppRootExtension, + createRouterExtension({ + namespace: 'test', + Component: ({ children }) => ( + {children} + ), + }), ], }), ], diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index a6783d43a3..a56ae2f140 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -56,9 +56,13 @@ describe('renderInTestApp', () => { fireEvent.click(screen.getByRole('link', { name: 'See details' })); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'See details', - }); + expect(analyticsApiMock.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: 'click', + subject: 'See details', + }), + ]), + ); }); }); diff --git a/plugins/auth-backend-module-microsoft-provider/api-report.md b/plugins/auth-backend-module-microsoft-provider/api-report.md index 21bf77d545..33600c10d8 100644 --- a/plugins/auth-backend-module-microsoft-provider/api-report.md +++ b/plugins/auth-backend-module-microsoft-provider/api-report.md @@ -10,9 +10,13 @@ import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; import { PassportProfile } from '@backstage/plugin-auth-node'; import { SignInResolverFactory } from '@backstage/plugin-auth-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export const authModuleMicrosoftProvider: () => BackendFeature; +// @public (undocumented) +const authModuleMicrosoftProvider_2: () => BackendFeature; +export default authModuleMicrosoftProvider_2; + // @public (undocumented) export const microsoftAuthenticator: OAuthAuthenticator< { diff --git a/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts new file mode 100644 index 0000000000..3c671b6c25 --- /dev/null +++ b/plugins/auth-backend-module-microsoft-provider/src/deprecated.ts @@ -0,0 +1,24 @@ +/* + * 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 { authModuleMicrosoftProvider as deprecatedAuthModuleMicrosoftProvider } from './module'; + +/** + * @public + * @deprecated Use default import instead + */ +export const authModuleMicrosoftProvider = + deprecatedAuthModuleMicrosoftProvider; diff --git a/plugins/auth-backend-module-microsoft-provider/src/index.ts b/plugins/auth-backend-module-microsoft-provider/src/index.ts index ae4fbf5926..45b7b719cd 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/index.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/index.ts @@ -21,5 +21,6 @@ */ export { microsoftAuthenticator } from './authenticator'; -export { authModuleMicrosoftProvider } from './module'; +export { authModuleMicrosoftProvider as default } from './module'; export { microsoftSignInResolvers } from './resolvers'; +export * from './deprecated'; diff --git a/plugins/auth-backend-module-pinniped-provider/api-report.md b/plugins/auth-backend-module-pinniped-provider/api-report.md index 430099713e..182ea8ba21 100644 --- a/plugins/auth-backend-module-pinniped-provider/api-report.md +++ b/plugins/auth-backend-module-pinniped-provider/api-report.md @@ -10,9 +10,13 @@ import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; import { Strategy } from 'openid-client'; import { TokenSet } from 'openid-client'; -// @public (undocumented) +// @public @deprecated (undocumented) export const authModulePinnipedProvider: () => BackendFeature; +// @public (undocumented) +const authModulePinnipedProvider_2: () => BackendFeature; +export default authModulePinnipedProvider_2; + // @public (undocumented) export const pinnipedAuthenticator: OAuthAuthenticator< PinnipedStrategyCache, diff --git a/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts new file mode 100644 index 0000000000..0dd2ed36bd --- /dev/null +++ b/plugins/auth-backend-module-pinniped-provider/src/deprecated.ts @@ -0,0 +1,23 @@ +/* + * 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 { authModulePinnipedProvider as deprecatedAuthModulePinnipedProvider } from './module'; + +/** + * @public + * @deprecated Use default import instead + */ +export const authModulePinnipedProvider = deprecatedAuthModulePinnipedProvider; diff --git a/plugins/auth-backend-module-pinniped-provider/src/index.ts b/plugins/auth-backend-module-pinniped-provider/src/index.ts index df4cd58603..f4b789644d 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/index.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/index.ts @@ -21,4 +21,5 @@ */ export { pinnipedAuthenticator, PinnipedStrategyCache } from './authenticator'; -export { authModulePinnipedProvider } from './module'; +export { authModulePinnipedProvider as default } from './module'; +export * from './deprecated'; diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index c9575ce5cd..e2489df4b4 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -213,4 +213,34 @@ describe('UrlReaderProcessor', () => { expect(reader.search).toHaveBeenCalledTimes(1); }); + + it('parser return valid URL with wildcard *', async () => { + const logger = getVoidLogger(); + + const reader: jest.Mocked = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn().mockImplementation(async () => []), + }; + + const processor = new UrlReaderProcessor({ reader, logger }); + + const emit = jest.fn(); + + await processor.readLocation( + { + type: 'url', + target: 'https://storage.cloud.google.com/ah-backstage-poc-catalog/*', + }, + false, + emit, + defaultEntityDataParser, + mockCache, + ); + + expect(reader.search).toHaveBeenCalledWith( + 'https://storage.cloud.google.com/ah-backstage-poc-catalog/*', + { etag: undefined }, + ); + }); }); diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index 5e31dcbd1a..c568f03a81 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -17,7 +17,6 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; -import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import { LocationSpec } from '@backstage/plugin-catalog-common'; @@ -124,7 +123,8 @@ export class UrlReaderProcessor implements CatalogProcessor { ): Promise<{ response: { data: Buffer; url: string }[]; etag?: string }> { // Does it contain globs? I.e. does it contain asterisks or question marks // (no curly braces for now) - const { filepath } = parseGitUrl(location); + + const { pathname: filepath } = new URL(location); if (filepath?.match(/[*?]/)) { const limiter = limiterFactory(5); const response = await this.options.reader.search(location, { etag }); 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 4f4e6ea55d..a2f6a00fe7 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -111,7 +111,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { } private async deleteMessages(messages?: Message[]): Promise { - if (!messages) { + if (!messages || messages.length === 0) { return; } @@ -129,7 +129,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { const result = await this.sqs.send( new DeleteMessageBatchCommand(deleteParams), ); - if (result.Failed) { + if (result.Failed && result.Failed.length > 0) { this.logger.error( `Failed to delete ${result.Failed!.length} of ${ messages.length diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 6fa02a7e6a..2fd980f003 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -42,6 +42,7 @@ 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' })); ``` @@ -49,6 +50,7 @@ Then update plugin environment to include the event broker. ```diff // packages/backend/src/types.ts ++ import { EventBroker } from '@backstage/plugin-events-node'; + eventBroker: EventBroker; ``` 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 23ac4bda45..c5b1038a01 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -377,7 +377,7 @@ describe('createPublishGithubPullRequestAction', () => { branchName: 'new-app', description: 'This PR is really good', reviewers: ['foobar'], - teamReviewers: ['team-foo'], + teamReviewers: ['team-foo', 'team-foo', 'team-bar'], }; mockDir.setContent({ [workspacePath]: {} }); @@ -401,7 +401,7 @@ describe('createPublishGithubPullRequestAction', () => { repo: 'myrepo', pull_number: 123, reviewers: ['foobar'], - team_reviewers: ['team-foo'], + team_reviewers: ['team-foo', 'team-bar'], }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 0763d4a474..2d8dc7ce8d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -372,7 +372,7 @@ export const createPublishGithubPullRequestAction = ( repo: pr.repo, pull_number: pr.number, reviewers, - team_reviewers: teamReviewers, + team_reviewers: teamReviewers ? [...new Set(teamReviewers)] : undefined, }); const addedUsers = result.data.requested_reviewers?.join(', ') ?? ''; const addedTeams = result.data.requested_teams?.join(', ') ?? ''; diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index 26f6a6a755..99b0e0b7cb 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -139,6 +139,24 @@ } ] }, + "presentation": { + "type": "object", + "description": "A way to redefine the labels for actionable buttons.", + "properties": { + "backButtonText": { + "type": "string", + "description": "A button which return the user to one step back." + }, + "createButtonText": { + "type": "string", + "description": "A button which start the execution of the template." + }, + "reviewButtonText": { + "type": "string", + "description": "A button which open the review step to verify the input prior to start the execution." + } + } + }, "EXPERIMENTAL_recovery": { "type": "object", "description": "A task recovery section.", diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx index 8366dcadf8..4c631b86b0 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.test.tsx @@ -25,7 +25,10 @@ describe('', () => { it('should render template output', async () => { const output = { links: [{ title: 'Link 1', url: 'https://backstage.io/' }], - text: [{ title: 'Text 1', content: 'Hello, **world**!' }], + text: [ + { title: 'Text 1', content: 'Hello, **world**!' }, + { title: 'Text 2', content: 'Hello, **mars**!' }, + ], }; const { getByRole } = await renderInTestApp( @@ -37,6 +40,11 @@ describe('', () => { }, ); + // first text output default visible + expect(getByRole('heading', { level: 2 }).innerHTML).toBe( + output.text[0].title, + ); + // test link outputs for (const link of output.links ?? []) { expect( diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index a495e4ca9e..c105c5a571 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -28,17 +28,18 @@ import { TextOutputs } from './TextOutputs'; export const DefaultTemplateOutputs = (props: { output?: ScaffolderTaskOutput; }) => { - const [textOutputIndex, setTextOutputIndex] = useState(); + const { output } = props; + const [textOutputIndex, setTextOutputIndex] = useState( + output?.text?.length ? 0 : undefined, + ); const textOutput = useMemo( () => - textOutputIndex !== undefined - ? props.output?.text?.[textOutputIndex] - : null, - [props.output, textOutputIndex], + textOutputIndex !== undefined ? output?.text?.[textOutputIndex] : null, + [output, textOutputIndex], ); - if (!props.output) { + if (!output) { return null; } @@ -48,11 +49,11 @@ export const DefaultTemplateOutputs = (props: { - + diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx index 7e64603e92..8f61aa6a30 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/TextOutputs.tsx @@ -47,7 +47,11 @@ export const TextOutputs = (props: { startIcon={} component="div" color="primary" - onClick={() => setIndex?.(index !== i ? i : undefined)} + onClick={() => { + if (index !== i) { + setIndex?.(i); + } + }} variant={index === i ? 'outlined' : undefined} > {title} diff --git a/yarn.lock b/yarn.lock index 8c0ef18c0d..2555a68326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3344,6 +3344,40 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-dynamic-feature-service@workspace:packages/backend-dynamic-feature-service": + version: 0.0.0-use.local + resolution: "@backstage/backend-dynamic-feature-service@workspace:packages/backend-dynamic-feature-service" + dependencies: + "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-events-backend": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-search-backend-node": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@backstage/types": "workspace:^" + "@types/express": ^4.17.6 + chokidar: ^3.5.3 + express: ^4.17.1 + lodash: ^4.17.21 + wait-for-expect: ^3.0.2 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/backend-openapi-utils@workspace:^, @backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils": version: 0.0.0-use.local resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils" @@ -3380,40 +3414,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/backend-plugin-manager@workspace:packages/backend-plugin-manager": - version: 0.0.0-use.local - resolution: "@backstage/backend-plugin-manager@workspace:packages/backend-plugin-manager" - dependencies: - "@backstage/backend-app-api": "workspace:^" - "@backstage/backend-common": "workspace:^" - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-tasks": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/config-loader": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/plugin-auth-node": "workspace:^" - "@backstage/plugin-catalog-backend": "workspace:^" - "@backstage/plugin-events-backend": "workspace:^" - "@backstage/plugin-events-node": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" - "@backstage/plugin-permission-node": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/plugin-search-backend-node": "workspace:^" - "@backstage/plugin-search-common": "workspace:^" - "@backstage/types": "workspace:^" - "@types/express": ^4.17.6 - chokidar: ^3.5.3 - express: ^4.17.1 - lodash: ^4.17.21 - wait-for-expect: ^3.0.2 - winston: ^3.2.1 - languageName: unknown - linkType: soft - "@backstage/backend-tasks@workspace:^, @backstage/backend-tasks@workspace:packages/backend-tasks": version: 0.0.0-use.local resolution: "@backstage/backend-tasks@workspace:packages/backend-tasks"