diff --git a/.changeset/bright-pants-cry.md b/.changeset/bright-pants-cry.md new file mode 100644 index 0000000000..88d7356640 --- /dev/null +++ b/.changeset/bright-pants-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed an issue where the CLI would fail to function when there was a mix of workspace and non-workspace versions of the same package in `yarn.lock` when using Yarn 3. diff --git a/.changeset/forty-years-retire.md b/.changeset/forty-years-retire.md new file mode 100644 index 0000000000..e7b887a1e4 --- /dev/null +++ b/.changeset/forty-years-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The frontend serve task now filters out allowed package duplicates during its package check, just like `versions:bump` and `versions:check`. diff --git a/.changeset/funny-pianos-grow.md b/.changeset/funny-pianos-grow.md new file mode 100644 index 0000000000..7823ca82b0 --- /dev/null +++ b/.changeset/funny-pianos-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed bug in `EntityTagPicker` that filtered on unavailable tags for the selected kind. diff --git a/.changeset/good-geckos-reflect.md b/.changeset/good-geckos-reflect.md new file mode 100644 index 0000000000..315675a3b4 --- /dev/null +++ b/.changeset/good-geckos-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Move some dependencies as `peerDependencies` because we need to always use same version as in `api-extractor` diff --git a/.changeset/metal-hotels-deliver.md b/.changeset/metal-hotels-deliver.md new file mode 100644 index 0000000000..6b7f9c109b --- /dev/null +++ b/.changeset/metal-hotels-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +add `onChange` handler to`Stepper` component diff --git a/.changeset/old-planets-care.md b/.changeset/old-planets-care.md new file mode 100644 index 0000000000..4b30ff7d2d --- /dev/null +++ b/.changeset/old-planets-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +`RouterOptions` now accepts an optional `catalogApi` argument, allowing custom catalog clients to be used. This is leveraged in the local standalone development runner to pass in a catalog client that returns fake data. diff --git a/.changeset/search-break-loop.md b/.changeset/search-break-loop.md new file mode 100644 index 0000000000..ce65645900 --- /dev/null +++ b/.changeset/search-break-loop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Fixed a bug that could cause an indexing process to silently fail, timeout, and accumulate stale indices. diff --git a/.changeset/search-mele-kalikimaka.md b/.changeset/search-mele-kalikimaka.md new file mode 100644 index 0000000000..4f1faff02f --- /dev/null +++ b/.changeset/search-mele-kalikimaka.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Fixed a bug that could cause the backstage backend to unexpectedly terminate when client errors were encountered during the indexing process. diff --git a/.changeset/search-pressure-back.md b/.changeset/search-pressure-back.md new file mode 100644 index 0000000000..3dd36456a7 --- /dev/null +++ b/.changeset/search-pressure-back.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Improved index throughput by optimizing when and how many documents were made available to the bulk client. diff --git a/.changeset/selfish-phones-smoke.md b/.changeset/selfish-phones-smoke.md new file mode 100644 index 0000000000..42f33f9869 --- /dev/null +++ b/.changeset/selfish-phones-smoke.md @@ -0,0 +1,37 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Added `catalogFilter` field to OwnerPicker and EntityPicker components to support filtering options by any field(s) of an entity. + +The `allowedKinds` field has been deprecated. Use `catalogFilter` instead. This field allows users to specify a filter on the shape of [EntityFilterQuery](https://github.com/backstage/backstage/blob/774c42003782121d3d6b2aa5f2865d53370c160e/packages/catalog-client/src/types/api.ts#L74), which can be passed into the CatalogClient. See examples below: + +- Get all entities of kind `Group` + + ```yaml + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + - kind: Group + ``` + +- Get entities of kind `Group` and spec.type `team` + ```yaml + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + - kind: Group + spec.type: team + ``` diff --git a/.changeset/strong-eyes-march.md b/.changeset/strong-eyes-march.md new file mode 100644 index 0000000000..4f3bbacfc8 --- /dev/null +++ b/.changeset/strong-eyes-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-ilert': patch +--- + +fixed error on service page not showing if historical uptime was disabled on a service diff --git a/.changeset/tasty-impalas-cross.md b/.changeset/tasty-impalas-cross.md new file mode 100644 index 0000000000..f7d7712b29 --- /dev/null +++ b/.changeset/tasty-impalas-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `repo test`, `repo lint`, and `repo build` commands will now analyze `yarn.lock` for dependency changes when searching for changed packages. This allows you to use the `--since ` flag even if you have `yarn.lock` changes. diff --git a/.changeset/thick-stingrays-smile.md b/.changeset/thick-stingrays-smile.md new file mode 100644 index 0000000000..2b56803ede --- /dev/null +++ b/.changeset/thick-stingrays-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Makes the `GroupsDiagram` not grown on screen on its own. diff --git a/.changeset/witty-eels-itch.md b/.changeset/witty-eels-itch.md new file mode 100644 index 0000000000..5a2da8b570 --- /dev/null +++ b/.changeset/witty-eels-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Workspace ranges are no longer considered invalid by version commands. diff --git a/.github/uffizzi/docker-compose.uffizzi.yml b/.github/uffizzi/docker-compose.uffizzi.yml index f505205505..49039cad2a 100644 --- a/.github/uffizzi/docker-compose.uffizzi.yml +++ b/.github/uffizzi/docker-compose.uffizzi.yml @@ -8,11 +8,11 @@ x-uffizzi: services: backstage: image: '${BACKSTAGE_IMAGE}' + environment: - POSTGRES_HOST: localhost + POSTGRES_HOST: db POSTGRES_USER: postgres POSTGRES_PASSWORD: example - GITHUB_TOKEN: abc NODE_ENV: production deploy: resources: @@ -21,7 +21,7 @@ services: entrypoint: '/bin/sh' command: - '-c' - - "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml --config app-config.production.yaml" + - "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml" db: image: postgres diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index d6df12a6af..56055c8342 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -2,9 +2,258 @@ app: title: Backstage Uffizzi Environment baseUrl: ${UFFIZZI_URL} +organization: + name: My Company + backend: baseUrl: ${UFFIZZI_URL} + auth: + keys: + # random mock key for uffizi deployments + - secret: 5TXvdjVZFxF7qf9K5RAYRDoGrLzJooqa + listen: + port: 7007 + database: + client: better-sqlite3 + connection: ':memory:' + cache: + store: memory cors: origin: ${UFFIZZI_URL} - methods: [GET, POST, PUT, DELETE] + methods: [GET, HEAD, PATCH, POST, PUT, DELETE] credentials: true + csp: + connect-src: ["'self'", 'http:', 'https:'] + # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference + # Default Helmet Content-Security-Policy values can be removed by setting the key to false + +auth: + environment: production + providers: {} + +catalog: + locations: + - type: url + target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml + +proxy: + '/circleci/api': + target: https://circleci.com/api/v1.1 + headers: + Circle-Token: ${CIRCLECI_AUTH_TOKEN} + + '/jenkins/api': + target: http://localhost:8080 + headers: + Authorization: ${JENKINS_BASIC_AUTH_HEADER} + + '/travisci/api': + target: https://api.travis-ci.com + changeOrigin: true + headers: + Authorization: ${TRAVISCI_AUTH_TOKEN} + travis-api-version: '3' + + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: ${NEW_RELIC_REST_API_KEY} + + '/newrelic/api': + target: https://api.newrelic.com + headers: + X-Api-Key: ${NEW_RELIC_USER_KEY} + + '/pagerduty': + target: https://api.pagerduty.com + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} + + '/buildkite/api': + target: https://api.buildkite.com/v2/ + headers: + Authorization: ${BUILDKITE_TOKEN} + + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: ${SENTRY_TOKEN} + + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: ${ILERT_AUTH_HEADER} + + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} + + '/gocd': + target: https://your.gocd.instance.com/go/api + allowedMethods: ['GET'] + allowedHeaders: ['Authorization'] + headers: + Authorization: Basic ${GOCD_AUTH_CREDENTIALS} + + '/dynatrace': + target: https://your.dynatrace.instance.com/api/v2 + headers: + Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' + +techdocs: + builder: 'local' # Alternatives - 'external' + generator: + runIn: 'docker' + # dockerImage: my-org/techdocs # use a custom docker image + # pullImage: true # or false to disable automatic pulling of image (e.g. if custom docker login is required) + publisher: + type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives. + +dynatrace: + baseUrl: https://your.dynatrace.instance.com + +# Score-cards sample configuration. +scorecards: + jsonDataUrl: https://raw.githubusercontent.com/Oriflame/backstage-plugins/main/plugins/score-card/sample-data/ + wikiLinkTemplate: https://link-to-wiki/{id} + +sentry: + organization: my-company + +rollbar: + organization: my-company + # NOTE: The rollbar-backend & accountToken key may be deprecated in the future (replaced by a proxy config) + accountToken: my-rollbar-account-token + +lighthouse: + baseUrl: http://localhost:3003 + +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: [] + +kafka: + clientId: backstage + clusters: + - name: cluster + dashboardUrl: https://akhq.io/ + brokers: + - localhost:9092 + +allure: + baseUrl: http://localhost:5050/allure-docker-service + +integrations: + github: + - host: github.com + token: ${GITHUB_TOKEN} + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} + ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): + # - host: ghe.example.net + # rawBaseUrl: https://ghe.example.net/raw + # token: ${GHE_TOKEN} + gitlab: + - host: gitlab.com + token: ${GITLAB_TOKEN} + ### Example for how to add a bitbucket cloud integration + # bitbucketCloud: + # - username: ${BITBUCKET_USERNAME} + # appPassword: ${BITBUCKET_APP_PASSWORD} + ### Example for how to add your bitbucket server instance using the API: + # - host: server.bitbucket.com + # apiBaseUrl: server.bitbucket.com + # username: ${BITBUCKET_SERVER_USERNAME} + # appPassword: ${BITBUCKET_SERVER_APP_PASSWORD} + azure: + - host: dev.azure.com + token: ${AZURE_TOKEN} + # googleGcs: + # clientEmail: 'example@example.com' + # privateKey: ${GCS_PRIVATE_KEY} + awsS3: + - host: amazonaws.com + accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} + +costInsights: + engineerCost: 200000 + engineerThreshold: 0.5 + products: + computeEngine: + name: Compute Engine + icon: compute + cloudDataflow: + name: Cloud Dataflow + icon: data + cloudStorage: + name: Cloud Storage + icon: storage + bigQuery: + name: BigQuery + icon: search + events: + name: Events + icon: data + metrics: + DAU: + name: Daily Active Users + default: true + MSC: + name: Monthly Subscribers + currencies: + engineers: + label: 'Engineers 🛠' + unit: 'engineer' + usd: + label: 'US Dollars 💵' + kind: 'USD' + unit: 'dollar' + prefix: '$' + rate: 1 + carbonOffsetTons: + label: 'Carbon Offset Tons ♻️⚖️s' + kind: 'CARBON_OFFSET_TONS' + unit: 'carbon offset ton' + rate: 3.5 + beers: + label: 'Beers 🍺' + kind: 'BEERS' + unit: 'beer' + rate: 4.5 + pintsIceCream: + label: 'Pints of Ice Cream 🍦' + kind: 'PINTS_OF_ICE_CREAM' + unit: 'ice cream pint' + rate: 5.5 +pagerduty: + eventsBaseUrl: 'https://events.pagerduty.com/v2' +jenkins: + instances: + - name: default + baseUrl: https://jenkins.example.com + username: backstage-bot + apiKey: 123456789abcdef0123456789abcedf012 + +azureDevOps: + host: dev.azure.com + token: my-token + organization: my-company + +apacheAirflow: + baseUrl: https://your.airflow.instance.com + +gocd: + baseUrl: https://your.gocd.instance.com + +permission: + enabled: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a37ae745c4..c4af6021b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,21 +185,10 @@ jobs: with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - - name: check for yarn.lock changes - id: yarn-lock - run: git diff --quiet origin/master HEAD -- yarn.lock - continue-on-error: true - - name: lint changed packages - if: ${{ steps.yarn-lock.outcome == 'success' }} run: yarn backstage-cli repo lint --since origin/master - - name: lint all packages - if: ${{ steps.yarn-lock.outcome == 'failure' }} - run: yarn backstage-cli repo lint - - name: test changed packages - if: ${{ steps.yarn-lock.outcome == 'success' }} run: yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --since origin/master env: BACKSTAGE_TEST_DISABLE_DOCKER: 1 @@ -207,17 +196,6 @@ jobs: BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored - - name: test all packages (and upload coverage) - if: ${{ steps.yarn-lock.outcome == 'failure' }} - run: | - yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=800M --coverage - bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD) - env: - BACKSTAGE_TEST_DISABLE_DOCKER: 1 - BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} - BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} - BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored - - name: ensure clean working directory run: | if files=$(git ls-files --exclude-standard --others --modified) && [[ -z "$files" ]]; then diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index 163531a755..1a86f8de71 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -8,6 +8,9 @@ on: jobs: build-backstage: + env: + NODE_OPTIONS: --max-old-space-size=4096 + UFFIZZI_URL: https://uffizzi.com name: Build PR image runs-on: ubuntu-latest if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} @@ -16,8 +19,6 @@ jobs: steps: - name: checkout uses: actions/checkout@v3 - with: - path: backstage - name: setup-node uses: actions/setup-node@v3 @@ -25,21 +26,22 @@ jobs: node-version: 16.x registry-url: https://registry.npmjs.org/ - - name: backstage create-app | delete if already exists - run: | - rm -rf ./example-app - npx @backstage/create-app - env: - BACKSTAGE_APP_NAME: example-app - - - name: yarn build - run: | - yarn build:all - working-directory: ./example-app + - name: yarn install + uses: backstage/actions/yarn-install@v0.5.12 + with: + cache-prefix: linux-v16 - name: Use Uffizzi's backstage app config run: | - cp ./backstage/.github/uffizzi/uffizzi.production.app-config.yaml ./example-app/app-config.production.yaml; + cp -f ./.github/uffizzi/uffizzi.production.app-config.yaml ./app-config.yaml + + - name: typescript build + run: | + yarn tsc + + - name: backstage build + run: | + yarn workspace example-backend build - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -58,8 +60,8 @@ jobs: - name: Build Image uses: docker/build-push-action@v2 with: - context: example-app - file: example-app/packages/backend/Dockerfile + context: . + file: packages/backend/Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} push: true diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index f0151d7618..383fd698f9 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -251,6 +251,23 @@ the entity belongs to the `"default"` namespace. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. +### `uid` [output] + +Each entity gets an automatically generated globally unique ID when it first +enters the database. This field is not meant to be specified as input data, but +is rater created by the database engine itself when producing the output entity. + +Note that `uid` values are _not_ to be seen as stable, and should _not_ be used +as external references to an entity. The `uid` can change over time even when a +human observer might think that it wouldn't. As one of many examples, +unregistering and re-registering the exact same file will result in a different +`uid` value even though everything else is the same. Therefore there is very +little, if any, reason to read or use this field externally. + +If you want to refer to an entity by some form of an identifier, you should +always use [string-form entity reference](references.md#string-references) +instead. + ### `title` [optional] A display name of the entity, to be presented in user interfaces instead of the diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index e5fdb034fb..71aa0ab758 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -44,8 +44,8 @@ spec: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group - title: Choose a location required: - repoUrl @@ -165,11 +165,11 @@ this: "firstName": { "ui:autofocus": true, "ui:emptyValue": "", - "ui:autocomplete": "family-name" + "ui:autocomplete": "given-name" }, "lastName": { "ui:emptyValue": "", - "ui:autocomplete": "given-name" + "ui:autocomplete": "family-name" }, "nicknames": { "ui:options":{ @@ -211,12 +211,12 @@ spec: default: Chuck ui:autofocus: true ui:emptyValue: '' - ui:autocomplete: family-name + ui:autocomplete: given-name lastName: type: string title: Last name ui:emptyValue: '' - ui:autocomplete: given-name + ui:autocomplete: family-name nicknames: type: array items: @@ -459,7 +459,7 @@ an owner for them. Ideally, users should be able to select an owner when they go through the scaffolder form from the users and groups already known to Backstage. The `OwnerPicker` is a custom field that generates a searchable list of groups and/or users already in the catalog to pick an owner from. You can -specify which of the two kinds are listed in the `allowedKinds` option: +specify which of the two kinds (or both) are listed in the `catalogFilter.kind` option: ```yaml owner: @@ -468,8 +468,8 @@ owner: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: [Group, User] ``` ## `spec.steps` - `Action[]` diff --git a/microsite-next/package.json b/microsite-next/package.json index 1b33d2896f..478fce0196 100644 --- a/microsite-next/package.json +++ b/microsite-next/package.json @@ -4,9 +4,8 @@ "license": "Apache-2.0", "private": true, "scripts": { - "examples": "docusaurus-examples", - "start": "docusaurus start", - "build": "docusaurus build", + "start": "node scripts/pre-build.js && docusaurus start", + "build": "node scripts/pre-build.js && docusaurus build", "prettier:check": "prettier --check .", "publish-gh-pages": "docusaurus-publish", "write-translations": "docusaurus-write-translations", @@ -20,7 +19,8 @@ "devDependencies": { "@spotify/prettier-config": "^14.0.0", "js-yaml": "^4.1.0", - "prettier": "^2.6.2" + "prettier": "^2.6.2", + "replace": "^1.2.2" }, "prettier": "@spotify/prettier-config", "dependencies": { diff --git a/microsite-next/scripts/pre-build.js b/microsite-next/scripts/pre-build.js new file mode 100644 index 0000000000..8b4fb4f71e --- /dev/null +++ b/microsite-next/scripts/pre-build.js @@ -0,0 +1,49 @@ +/* + * 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. + */ + +const replace = require('replace'); +const { existsSync, writeFileSync, mkdirSync } = require('fs'); +const path = require('path'); + +const PLACEHOLDER = `--- +id: "index" +title: "Package Index" +description: "Index of all Backstage Packages" +--- +Run \`yarn build:api-docs\` to generate the API docs. +`; + +async function main() { + const referencesDir = '../docs/reference'; + if (existsSync(referencesDir)) { + console.log('Removing HTML comments from docs/reference folder'); + replace({ + regex: '', + replacement: '', + paths: [referencesDir], + recursive: true, + silent: false, + }); + } else { + mkdirSync(referencesDir); + writeFileSync(path.join(referencesDir, 'index.md'), PLACEHOLDER); + } +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/microsite-next/sidebars.json b/microsite-next/sidebars.json index 0967ef424b..67c4209976 100644 --- a/microsite-next/sidebars.json +++ b/microsite-next/sidebars.json @@ -1 +1,366 @@ -{} +{ + "releases": { + "Release Notes": [ + "releases/v1.8.0", + "releases/v1.7.0", + "releases/v1.6.0", + "releases/v1.5.0", + "releases/v1.4.0", + "releases/v1.3.0", + "releases/v1.2.0", + "releases/v1.1.0", + "releases/v1.0.0" + ] + }, + "docs": { + "Overview": [ + "overview/what-is-backstage", + "overview/architecture-overview", + "overview/roadmap", + "overview/vision", + "overview/background", + "overview/adopting", + "overview/versioning-policy", + "overview/threat-model", + "overview/support", + "overview/glossary", + "overview/logos" + ], + "Getting Started": [ + "getting-started/index", + "getting-started/configuration", + "getting-started/create-an-app", + "getting-started/running-backstage-locally", + { + "type": "category", + "label": "App configuration", + "items": [ + "getting-started/configure-app-with-plugins", + "getting-started/app-custom-theme", + "getting-started/homepage" + ] + }, + "getting-started/keeping-backstage-updated", + "getting-started/concepts", + "getting-started/contributors", + "getting-started/project-structure" + ], + "Local Development": [ + { + "type": "category", + "label": "CLI", + "items": [ + "local-dev/cli-overview", + "local-dev/cli-build-system", + "local-dev/cli-commands" + ] + }, + "local-dev/linking-local-packages" + ], + "Core Features": [ + { + "type": "category", + "label": "Software Catalog", + "items": [ + "features/software-catalog/software-catalog-overview", + "features/software-catalog/life-of-an-entity", + "features/software-catalog/configuration", + "features/software-catalog/system-model", + "features/software-catalog/descriptor-format", + "features/software-catalog/references", + "features/software-catalog/well-known-annotations", + "features/software-catalog/well-known-relations", + "features/software-catalog/well-known-statuses", + "features/software-catalog/extending-the-model", + "features/software-catalog/external-integrations", + "features/software-catalog/catalog-customization", + "features/software-catalog/software-catalog-api" + ] + }, + { + "type": "category", + "label": "Kubernetes", + "items": [ + "features/kubernetes/overview", + "features/kubernetes/installation", + "features/kubernetes/configuration", + "features/kubernetes/authentication", + "features/kubernetes/troubleshooting" + ] + }, + { + "type": "category", + "label": "Software Templates", + "items": [ + "features/software-templates/software-templates-index", + "features/software-templates/configuration", + "features/software-templates/adding-templates", + "features/software-templates/writing-templates", + "features/software-templates/input-examples", + "features/software-templates/builtin-actions", + "features/software-templates/writing-custom-actions", + "features/software-templates/writing-custom-field-extensions", + "features/software-templates/writing-custom-step-layouts", + "features/software-templates/migrating-from-v1beta2-to-v1beta3" + ] + }, + { + "type": "category", + "label": "Backstage Search", + "items": [ + "features/search/search-overview", + "features/search/getting-started", + "features/search/concepts", + "features/search/architecture", + "features/search/search-engines", + "features/search/how-to-guides" + ] + }, + { + "type": "category", + "label": "TechDocs", + "items": [ + "features/techdocs/techdocs-overview", + "features/techdocs/getting-started", + "features/techdocs/concepts", + "features/techdocs/addons", + "features/techdocs/architecture", + "features/techdocs/creating-and-publishing", + "features/techdocs/configuration", + "features/techdocs/using-cloud-storage", + "features/techdocs/configuring-ci-cd", + "features/techdocs/cli", + "features/techdocs/how-to-guides", + "features/techdocs/troubleshooting", + "features/techdocs/faqs" + ] + } + ], + "Integrations": [ + "integrations/index", + { + "type": "category", + "label": "AWS S3", + "items": [ + "integrations/aws-s3/locations", + "integrations/aws-s3/discovery" + ] + }, + { + "type": "category", + "label": "Azure", + "items": [ + "integrations/azure/locations", + "integrations/azure/discovery", + "integrations/azure/org" + ] + }, + { + "type": "category", + "label": "Bitbucket Cloud", + "items": [ + "integrations/bitbucketCloud/locations", + "integrations/bitbucketCloud/discovery" + ] + }, + { + "type": "category", + "label": "Bitbucket Server", + "items": [ + "integrations/bitbucketServer/locations", + "integrations/bitbucketServer/discovery" + ] + }, + { + "type": "category", + "label": "Datadog", + "items": ["integrations/datadog-rum/installation"] + }, + { + "type": "category", + "label": "Gerrit", + "items": [ + "integrations/gerrit/locations", + "integrations/gerrit/discovery" + ] + }, + { + "type": "category", + "label": "GitHub", + "items": [ + "integrations/github/locations", + "integrations/github/discovery", + "integrations/github/org", + "integrations/github/github-apps" + ] + }, + { + "type": "category", + "label": "GitLab", + "items": [ + "integrations/gitlab/locations", + "integrations/gitlab/discovery" + ] + }, + { + "type": "category", + "label": "Gitea", + "items": ["integrations/gitea/locations"] + }, + { + "type": "category", + "label": "Google GCS", + "items": ["integrations/google-cloud-storage/locations"] + }, + { + "type": "category", + "label": "LDAP", + "items": ["integrations/ldap/org"] + } + ], + "Plugins": [ + "plugins/index", + "plugins/existing-plugins", + "plugins/create-a-plugin", + "plugins/plugin-development", + "plugins/structure-of-a-plugin", + "plugins/integrating-plugin-into-software-catalog", + "plugins/integrating-search-into-plugins", + "plugins/composability", + "plugins/customization", + "plugins/analytics", + "plugins/feature-flags", + { + "type": "category", + "label": "Backends and APIs", + "items": [ + "plugins/proxying", + "plugins/backend-plugin", + "plugins/call-existing-api", + "plugins/url-reader", + "plugins/new-backend-system" + ] + }, + { + "type": "category", + "label": "Testing", + "items": ["plugins/testing"] + }, + { + "type": "category", + "label": "Publishing", + "items": [ + "plugins/publish-private", + "plugins/add-to-marketplace", + "plugins/observability" + ] + } + ], + "Configuration": [ + "conf/index", + "conf/reading", + "conf/writing", + "conf/defining" + ], + "Auth and identity": [ + "auth/index", + { + "type": "category", + "label": "Included providers", + "items": [ + "auth/auth0/provider", + "auth/atlassian/provider", + "auth/bitbucket/provider", + "auth/microsoft/provider", + "auth/github/provider", + "auth/gitlab/provider", + "auth/google/provider", + "auth/google/gcp-iap-auth", + "auth/okta/provider", + "auth/onelogin/provider", + "auth/oauth2-proxy/provider" + ] + }, + "auth/identity-resolver", + "auth/oauth", + "auth/oidc", + "auth/add-auth-provider", + "auth/service-to-service-auth", + "auth/troubleshooting", + "auth/glossary" + ], + "Permissions": [ + "permissions/overview", + "permissions/concepts", + "permissions/getting-started", + "permissions/writing-a-policy", + "permissions/frontend-integration", + "permissions/custom-rules", + { + "type": "category", + "label": "Tutorial: using Permissions in your plugin", + "items": [ + "permissions/plugin-authors/01-setup", + "permissions/plugin-authors/02-adding-a-basic-permission-check", + "permissions/plugin-authors/03-adding-a-resource-permission-check", + "permissions/plugin-authors/04-authorizing-access-to-paginated-data", + "permissions/plugin-authors/05-frontend-authorization" + ] + } + ], + "Deployment": [ + "deployment/index", + "deployment/scaling", + "deployment/docker", + "deployment/k8s", + "deployment/heroku" + ], + "Designing for Backstage": [ + "dls/design", + "dls/component-design-guidelines", + "dls/contributing-to-storybook", + "dls/figma" + ], + "API Reference": [ + { + "type": "category", + "label": "Guides", + "items": ["api/utility-apis"] + }, + { + "type": "category", + "label": "API Reference", + "items": ["reference/index"] + }, + "api/deprecations" + ], + "Tutorials": [ + "tutorials/journey", + "tutorials/quickstart-app-plugin", + "tutorials/react-router-stable-migration", + "tutorials/package-role-migration", + "tutorials/migrating-away-from-core", + "tutorials/configuring-plugin-databases", + "tutorials/switching-sqlite-postgres", + "tutorials/using-backstage-proxy-within-plugin", + "tutorials/yarn-migration" + ], + "Architecture Decision Records (ADRs)": [ + "architecture-decisions/adrs-overview", + "architecture-decisions/adrs-adr001", + "architecture-decisions/adrs-adr002", + "architecture-decisions/adrs-adr003", + "architecture-decisions/adrs-adr004", + "architecture-decisions/adrs-adr005", + "architecture-decisions/adrs-adr006", + "architecture-decisions/adrs-adr007", + "architecture-decisions/adrs-adr008", + "architecture-decisions/adrs-adr009", + "architecture-decisions/adrs-adr010", + "architecture-decisions/adrs-adr011", + "architecture-decisions/adrs-adr012", + "architecture-decisions/adrs-adr013" + ], + "FAQ": ["FAQ"] + } +} diff --git a/microsite-next/yarn.lock b/microsite-next/yarn.lock index fa35b93945..c7dcd41c28 100644 --- a/microsite-next/yarn.lock +++ b/microsite-next/yarn.lock @@ -3573,6 +3573,7 @@ __metadata: prettier: ^2.6.2 react: ^17.0.2 react-dom: ^17.0.2 + replace: ^1.2.2 languageName: unknown linkType: soft @@ -3827,6 +3828,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^5.0.0": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + "camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" @@ -3860,7 +3868,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.0.0": +"chalk@npm:2.4.2, chalk@npm:^2.0.0": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -4021,6 +4029,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^6.2.0 + checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42 + languageName: node + linkType: hard + "clone-deep@npm:^4.0.1": version: 4.0.1 resolution: "clone-deep@npm:4.0.1" @@ -4597,6 +4616,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decompress-response@npm:^3.3.0": version: 3.3.0 resolution: "decompress-response@npm:3.3.0" @@ -5355,7 +5381,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.0.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -5544,6 +5570,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.1": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1": version: 1.1.3 resolution: "get-intrinsic@npm:1.1.3" @@ -7164,6 +7197,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:3.0.5": + version: 3.0.5 + resolution: "minimatch@npm:3.0.5" + dependencies: + brace-expansion: ^1.1.7 + checksum: a3b84b426eafca947741b864502cee02860c4e7b145de11ad98775cfcf3066fef422583bc0ffce0952ddf4750c1ccf4220b1556430d4ce10139f66247d87d69e + languageName: node + linkType: hard + "minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -8982,6 +9024,27 @@ __metadata: languageName: node linkType: hard +"replace@npm:^1.2.2": + version: 1.2.2 + resolution: "replace@npm:1.2.2" + dependencies: + chalk: 2.4.2 + minimatch: 3.0.5 + yargs: ^15.3.1 + bin: + replace: bin/replace.js + search: bin/search.js + checksum: 1d69f43937a5fdf9dea278e78d6f3b51c1889ba5135bd201918bbda6330684adf8276e8e90e1c021034420dd4df239e51c23ca40752cb9bc6180c153d6d46a37 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -8996,6 +9059,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -10623,6 +10693,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.0 + resolution: "which-module@npm:2.0.0" + checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c + languageName: node + linkType: hard + "which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" @@ -10679,6 +10756,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a + languageName: node + linkType: hard + "wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -10775,6 +10863,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -10789,6 +10884,35 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: ^5.0.0 + decamelize: ^1.2.0 + checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9 + languageName: node + linkType: hard + +"yargs@npm:^15.3.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: ^6.0.0 + decamelize: ^1.2.0 + find-up: ^4.1.0 + get-caller-file: ^2.0.1 + require-directory: ^2.1.1 + require-main-filename: ^2.0.0 + set-blocking: ^2.0.0 + string-width: ^4.2.0 + which-module: ^2.0.0 + y18n: ^4.0.0 + yargs-parser: ^18.1.2 + checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" diff --git a/package.json b/package.json index 6e99f4f176..f276d42416 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "techdocs-cli": "node scripts/techdocs-cli.js", "techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js", "prepare": "husky install", - "postinstall": "husky install" + "postinstall": "husky install || true" }, "workspaces": { "packages": [ diff --git a/packages/app/src/components/scaffolder/defaultPreviewTemplate.ts b/packages/app/src/components/scaffolder/defaultPreviewTemplate.ts index 56f34779de..5d8a2accda 100644 --- a/packages/app/src/components/scaffolder/defaultPreviewTemplate.ts +++ b/packages/app/src/components/scaffolder/defaultPreviewTemplate.ts @@ -31,8 +31,8 @@ parameters: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group - title: Choose a location required: - repoUrl diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 21b1580cb9..a6bf87da82 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -84,6 +84,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const graph = PackageGraph.fromPackages(packages); const changedPackages = await graph.listChangedPackages({ ref: opts.since, + analyzeLockfile: true, }); const withDevDependents = graph.collectPackageNames( changedPackages.map(pkg => pkg.name), diff --git a/packages/cli/src/commands/repo/lint.ts b/packages/cli/src/commands/repo/lint.ts index 61d85b3773..8e8ec20ff8 100644 --- a/packages/cli/src/commands/repo/lint.ts +++ b/packages/cli/src/commands/repo/lint.ts @@ -34,7 +34,10 @@ export async function command(opts: OptionValues): Promise { if (opts.since) { const graph = PackageGraph.fromPackages(packages); - packages = await graph.listChangedPackages({ ref: opts.since }); + packages = await graph.listChangedPackages({ + ref: opts.since, + analyzeLockfile: true, + }); } // Packages are ordered from most to least number of dependencies, as a diff --git a/packages/cli/src/commands/repo/test.ts b/packages/cli/src/commands/repo/test.ts index 578cb15e83..95bfe8f449 100644 --- a/packages/cli/src/commands/repo/test.ts +++ b/packages/cli/src/commands/repo/test.ts @@ -93,6 +93,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const graph = PackageGraph.fromPackages(packages); const changedPackages = await graph.listChangedPackages({ ref: opts.since, + analyzeLockfile: true, }); const packageNames = Array.from( diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index f6923632c8..b7b81e6bff 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -21,7 +21,8 @@ import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; import { Lockfile } from '../../lib/versioning'; -import { includedFilter } from '../versions/lint'; +import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; +import { PackageGraph } from '../../lib/monorepo'; interface StartAppOptions { verifyVersions?: boolean; @@ -36,10 +37,13 @@ export async function startFrontend(options: StartAppOptions) { const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); const result = lockfile.analyze({ filter: includedFilter, + localPackages: PackageGraph.fromPackages( + await PackageGraph.listTargetPackages(), + ), }); - const problemPackages = [...result.newVersions, ...result.newRanges].map( - ({ name }) => name, - ); + const problemPackages = [...result.newVersions, ...result.newRanges] + .map(({ name }) => name) + .filter(forbiddenDuplicatesFilter); if (problemPackages.length > 1) { console.log( diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 1905aded20..45b966a52d 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -39,6 +39,7 @@ import { ReleaseManifest, } from '@backstage/release-manifests'; import 'global-agent/bootstrap'; +import { PackageGraph } from '../../lib/monorepo'; const DEP_TYPES = [ 'dependencies', @@ -200,7 +201,7 @@ export default async (opts: OptionValues) => { lockfile.remove(name, range); } } - await lockfile.save(); + await lockfile.save(lockfilePath); } const breakingUpdates = new Map(); @@ -305,8 +306,12 @@ export default async (opts: OptionValues) => { // Finally we make sure the new lockfile doesn't have any duplicates const dedupLockfile = await Lockfile.load(lockfilePath); + const result = dedupLockfile.analyze({ filter, + localPackages: PackageGraph.fromPackages( + await PackageGraph.listTargetPackages(), + ), }); if (result.newVersions.length > 0) { diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index 9939b2ba51..8de38548a9 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -18,6 +18,7 @@ import { OptionValues } from 'commander'; import { Lockfile } from '../../lib/versioning'; import { paths } from '../../lib/paths'; import partition from 'lodash/partition'; +import { PackageGraph } from '../../lib/monorepo'; // Packages that we try to avoid duplicates for const INCLUDED = [/^@backstage\//]; @@ -52,9 +53,13 @@ export default async (cmd: OptionValues) => { let success = true; - const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); const result = lockfile.analyze({ filter: includedFilter, + localPackages: PackageGraph.fromPackages( + await PackageGraph.listTargetPackages(), + ), }); logArray( @@ -65,7 +70,7 @@ export default async (cmd: OptionValues) => { if (fix) { lockfile.replaceVersions(result.newVersions); - await lockfile.save(); + await lockfile.save(lockfilePath); } else { const [newVersionsForbidden, newVersionsAllowed] = partition( result.newVersions, diff --git a/packages/cli/src/lib/git.ts b/packages/cli/src/lib/git.ts index 074260b38f..c0b50f5140 100644 --- a/packages/cli/src/lib/git.ts +++ b/packages/cli/src/lib/git.ts @@ -64,3 +64,23 @@ export async function listChangedFiles(ref: string) { return Array.from(new Set([...tracked, ...untracked])); } + +/** + * Returns the contents of a file at a specific ref. + */ +export async function readFileAtRef(path: string, ref: string) { + let showRef = ref; + try { + const [base] = await runGit('merge-base', 'HEAD', ref); + showRef = base; + } catch { + // silently fall back to using the ref directly if merge base is not available + } + + const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], { + shell: true, + cwd: paths.targetRoot, + maxBuffer: 1024 * 1024 * 50, + }); + return stdout; +} diff --git a/packages/cli/src/lib/monorepo/PackageGraph.test.ts b/packages/cli/src/lib/monorepo/PackageGraph.test.ts index feb1709bf7..d8edff9c81 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.test.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.test.ts @@ -14,19 +14,25 @@ * limitations under the License. */ +import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from './PackageGraph'; -import { listChangedFiles } from '../git'; +import { Lockfile } from '../versioning/Lockfile'; +import { listChangedFiles, readFileAtRef } from '../git'; jest.mock('../git'); const mockListChangedFiles = listChangedFiles as jest.MockedFunction< typeof listChangedFiles >; +const mockReadFileAtRef = readFileAtRef as jest.MockedFunction< + typeof readFileAtRef +>; jest.mock('../paths', () => ({ paths: { targetRoot: '/', + resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths), }, })); @@ -177,4 +183,54 @@ describe('PackageGraph', () => { graph.listChangedPackages({ ref: 'origin/master' }), ).resolves.toEqual([graph.get('a'), graph.get('b')]); }); + + it('lists changed packages with lockfile analysis', async () => { + const graph = PackageGraph.fromPackages(testPackages); + + mockListChangedFiles.mockResolvedValueOnce( + ['README.md', 'packages/a/src/foo.ts', 'yarn.lock'].sort(), + ); + mockReadFileAtRef.mockResolvedValueOnce(` +a@^1: + version: "1.0.0" + +c@^1: + version: "1.0.0" + dependencies: + c-dep: ^1 + +c-dep@^2: + version: "2.0.0" + integrity: sha512-xyz +`); + jest.spyOn(Lockfile, 'load').mockResolvedValueOnce( + Lockfile.parse(` +a@^1: + version: "1.0.0" + +c@^1: + version: "1.0.0" + dependencies: + c-dep: ^1 + +c-dep@^2: + version: "2.0.0" + integrity: sha512-xyz-other +`), + ); + + await expect( + graph + .listChangedPackages({ + ref: 'origin/master', + analyzeLockfile: true, + }) + .then(pkgs => pkgs.map(pkg => pkg.name)), + ).resolves.toEqual(['a', 'c']); + + expect(mockReadFileAtRef).toHaveBeenCalledWith( + 'yarn.lock', + 'origin/master', + ); + }); }); diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index c65f5c003c..a1fc95e669 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -18,7 +18,8 @@ import path from 'path'; import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; import { PackageRole } from '../role'; -import { listChangedFiles } from '../git'; +import { listChangedFiles, readFileAtRef } from '../git'; +import { Lockfile } from '../versioning'; type PackageJSON = Package['packageJson']; @@ -191,7 +192,10 @@ export class PackageGraph extends Map { return targets; } - async listChangedPackages(options: { ref: string }) { + async listChangedPackages(options: { + ref: string; + analyzeLockfile?: boolean; + }) { const changedFiles = await listChangedFiles(options.ref); const dirMap = new Map( @@ -234,6 +238,77 @@ export class PackageGraph extends Map { } } + if (changedFiles.includes('yarn.lock') && options.analyzeLockfile) { + // Load the lockfile in the working tree and the one at the ref and diff them + let thisLockfile: Lockfile; + let otherLockfile: Lockfile; + try { + thisLockfile = await Lockfile.load( + paths.resolveTargetRoot('yarn.lock'), + ); + otherLockfile = Lockfile.parse( + await readFileAtRef('yarn.lock', options.ref), + ); + } catch (error) { + console.warn( + `Failed to read lockfiles, assuming all packages have changed, ${error}`, + ); + return Array.from(this.values()); + } + const diff = thisLockfile.diff(otherLockfile); + + // Create a simplified dependency graph only keeps track of package names + const graph = thisLockfile.createSimplifiedDependencyGraph(); + + // Merge the dependency graph from the other lockfile into this one in + // order to be able to detect removals accurately. + { + const otherGraph = thisLockfile.createSimplifiedDependencyGraph(); + for (const [name, dependencies] of otherGraph) { + const node = graph.get(name); + if (node) { + dependencies.forEach(d => node.add(d)); + } else { + graph.set(name, dependencies); + } + } + } + + // The check is simplified by only considering the package names rather + // than the exact version range queries that were changed. + // TODO(Rugvip): Use a more exact check + const changedPackages = new Set( + [...diff.added, ...diff.changed, ...diff.removed].map(e => e.name), + ); + + // Starting with our set of changed packages from the diff, we loop through + // the full graph and add any package that has a dependency on a changed package. + // We keep looping until all transitive dependencies have been detected. + let changed = false; + do { + changed = false; + for (const [name, dependencies] of graph) { + if (changedPackages.has(name)) { + continue; + } + for (const dep of dependencies) { + if (changedPackages.has(dep)) { + changed = true; + changedPackages.add(name); + break; + } + } + } + } while (changed); + + // Add all local packages that had a transitive dependency change to the result set + for (const node of this.values()) { + if (changedPackages.has(node.name) && !result.includes(node)) { + result.push(node); + } + } + } + return result; } } diff --git a/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts b/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts index 03be32dbaa..50fef44a67 100644 --- a/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts +++ b/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import { paths } from '../../paths'; import { Task } from '../../tasks'; import { FactoryRegistry } from '../FactoryRegistry'; @@ -73,7 +73,7 @@ describe('webLibraryPackage factory', () => { '', `Creating web-library package ${expectedwebLibraryPackageName}`, 'Checking Prerequisites:', - `availability packages/${expectedwebLibraryPackageName}`, + `availability ${joinPath('packages', expectedwebLibraryPackageName)}`, 'creating temp dir', 'Executing Template:', 'copying .eslintrc.js', @@ -82,7 +82,7 @@ describe('webLibraryPackage factory', () => { 'templating index.ts.hbs', 'copying setupTests.ts', 'Installing:', - `moving packages/${expectedwebLibraryPackageName}`, + `moving ${joinPath('packages', expectedwebLibraryPackageName)}`, ]); await expect( diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index 4621b43372..0275c5ee50 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -16,14 +16,23 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; +import { ExtendedPackage } from '../monorepo'; import { Lockfile } from './Lockfile'; -const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 `; -const mockA = `${HEADER} +const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 +`; + +const mockA = `${LEGACY_HEADER} a@^1: version "1.0.1" resolved "https://my-registry/a-1.0.01.tgz#abc123" @@ -38,7 +47,7 @@ b@^2: version "2.0.0" `; -const mockADedup = `${HEADER} +const mockADedup = `${LEGACY_HEADER} a@^1: version "1.0.1" resolved "https://my-registry/a-1.0.01.tgz#abc123" @@ -50,7 +59,7 @@ b@2.0.x, b@^2: version "2.0.1" `; -const mockB = `${HEADER} +const mockB = `${LEGACY_HEADER} "@s/a@*", "@s/a@1 || 2", "@s/a@^1": version "1.0.1" @@ -58,7 +67,7 @@ const mockB = `${HEADER} version "2.0.0" `; -const mockBDedup = `${HEADER} +const mockBDedup = `${LEGACY_HEADER} "@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x": version "2.0.0" @@ -77,10 +86,12 @@ describe('Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1' }, - { range: '^2', version: '2.0.0' }, + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, ]); expect(lockfile.toString()).toBe(mockA); }); @@ -91,7 +102,7 @@ describe('Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze(); + const result = lockfile.analyze({ localPackages: new Map() }); expect(result).toEqual({ invalidRanges: [], newRanges: [], @@ -110,7 +121,7 @@ describe('Lockfile', () => { expect(lockfile.toString()).toBe(mockADedup); await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA); - await expect(lockfile.save()).resolves.toBeUndefined(); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup); }); @@ -120,7 +131,7 @@ describe('Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze(); + const result = lockfile.analyze({ localPackages: new Map() }); expect(result).toEqual({ invalidRanges: [], newRanges: [ @@ -154,15 +165,7 @@ describe('Lockfile', () => { }); }); -const newHeader = `# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 -`; - -const mockANew = `${newHeader} +const mockANew = `${MODERN_HEADER} a@^1: version: 1.0.1 dependencies: @@ -177,7 +180,7 @@ b@^2: version: 2.0.0 `; -const mockANewDedup = `${newHeader} +const mockANewDedup = `${MODERN_HEADER} a@^1: version: 1.0.1 dependencies: @@ -192,6 +195,36 @@ b@^2: version: 2.0.1 `; +const mockANewLocal = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 0.0.0-use.local + +b@^2: + version: 2.0.0 +`; + +const mockANewLocalDedup = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 0.0.0-use.local + +b@^2: + version: 0.0.0-use.local +`; + describe('New Lockfile', () => { afterEach(() => { mockFs.restore(); @@ -203,11 +236,13 @@ describe('New Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1' }, - { range: '^2.0.1', version: '2.0.1' }, - { range: '^2', version: '2.0.0' }, + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, ]); expect(lockfile.toString()).toBe(mockANew); }); @@ -218,7 +253,7 @@ describe('New Lockfile', () => { }); const lockfile = await Lockfile.load('/yarn.lock'); - const result = lockfile.analyze(); + const result = lockfile.analyze({ localPackages: new Map() }); expect(result).toEqual({ invalidRanges: [], newRanges: [], @@ -237,9 +272,515 @@ describe('New Lockfile', () => { expect(lockfile.toString()).toBe(mockANewDedup); await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew); - await expect(lockfile.save()).resolves.toBeUndefined(); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( mockANewDedup, ); }); + + it('should deduplicate and save mockANewLocal', async () => { + mockFs({ + '/yarn.lock': mockANewLocal, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze({ + localPackages: new Map([ + [ + 'b', + { + packageJson: { version: '2.0.1' }, + } as ExtendedPackage, + ], + ]), + }); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [], + newVersions: [ + { + name: 'b', + range: '^2', + oldVersion: '2.0.0', + newVersion: '0.0.0-use.local', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockANewLocal); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockANewLocalDedup); + + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewLocal, + ); + await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe( + mockANewLocalDedup, + ); + }); + + describe('diff', () => { + const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@3: + version "3.0.1" + integrity sha512-abc1 + +b@2.0.x: + version "2.0.1" + integrity sha512-abc2 + +b@^2: + version "2.0.0" + integrity sha512-abc3 + +c@^1: + version "1.0.1" + integrity x +`); + + const lockfileLegacyB = Lockfile.parse(`${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz-other + dependencies: + b "^2" + +b@2.0.x, b@^2: + version "2.0.0" + integrity sha512-abc3 + +b@4: + version "4.0.0" + integrity sha512-abc + +d@^1: + version "1.0.1" + integrity x +`); + + const lockfileModernA = Lockfile.parse(`${MODERN_HEADER} +"a@npm:^1": + version: "1.0.1" + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + checksum: sha512-xyz + dependencies: + b: "^2" + +"b@npm:3": + version: "3.0.1" + checksum: sha512-abc1 + +"b@npm:2.0.x": + version: "2.0.1" + checksum: sha512-abc2 + +"b@npm:^2": + version: "2.0.0" + checksum: sha512-abc3 + +"c@npm:^1": + version: "1.0.1" + checksum: x +`); + + const lockfileModernB = Lockfile.parse(`${MODERN_HEADER} +"a@npm:^1": + version: "1.0.1" + resolution: "a@npm:1.0.1" + checksum: sha512-xyz-other + dependencies: + b: "^2" + +"b@npm:2.0.x, b@npm:^2": + version: "2.0.0" + checksum: sha512-abc3 + +"b@npm:4": + version: "4.0.0" + checksum: sha512-abc + +"d@npm:^1": + version: "1.0.1" + checksum: x +`); + + it('should diff two legacy lockfiles', async () => { + expect(lockfileLegacyA.diff(lockfileLegacyB)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + expect(lockfileLegacyB.diff(lockfileLegacyA)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + }); + + it('should diff two modern lockfiles', async () => { + expect(lockfileModernA.diff(lockfileModernB)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + expect(lockfileModernB.diff(lockfileModernA)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + }); + + it('should diff legacy and modern lockfiles', async () => { + expect(lockfileLegacyA.diff(lockfileModernB)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + expect(lockfileLegacyB.diff(lockfileModernA)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + }); + + it('should diff modern and legacy lockfiles', async () => { + expect(lockfileModernA.diff(lockfileLegacyB)).toEqual({ + added: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + }); + expect(lockfileModernB.diff(lockfileLegacyA)).toEqual({ + added: [ + { name: 'b', range: '4' }, + { name: 'd', range: '^1' }, + ], + changed: [ + { name: 'a', range: '^1' }, + { name: 'b', range: '2.0.x' }, + ], + removed: [ + { name: 'b', range: '3' }, + { name: 'c', range: '^1' }, + ], + }); + }); + + it('should handle workspace ranges', async () => { + const lockfile = `${MODERN_HEADER} +"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults": + version: 0.0.0-use.local + resolution: "@backstage/app-defaults@workspace:packages/app-defaults" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@types/node": ^16.11.26 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + +"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api": + version: 0.0.0-use.local + resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + express: ^4.17.1 + express-promise-router: ^4.1.0 + winston: ^3.2.1 + languageName: unknown + linkType: soft +`; + expect(Lockfile.parse(lockfile).diff(Lockfile.parse(lockfile))).toEqual({ + added: [], + changed: [], + removed: [], + }); + }); + }); + + describe('createSimplifiedDependencyGraph', () => { + it('for modern lockfile', () => { + expect( + Lockfile.parse( + `${MODERN_HEADER} +"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults": + version: 0.0.0-use.local + resolution: "@backstage/app-defaults@workspace:packages/app-defaults" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@types/node": ^16.11.26 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + +"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api": + version: 0.0.0-use.local + resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + express: ^4.17.1 + express-promise-router: ^4.1.0 + winston: ^3.2.1 + languageName: unknown + linkType: soft +`, + ).createSimplifiedDependencyGraph(), + ).toEqual( + new Map([ + [ + '@backstage/app-defaults', + new Set([ + '@backstage/cli', + '@backstage/core-app-api', + '@backstage/core-components', + '@backstage/core-plugin-api', + '@backstage/plugin-permission-react', + '@backstage/test-utils', + '@backstage/theme', + '@material-ui/core', + '@material-ui/icons', + '@testing-library/jest-dom', + '@testing-library/react', + '@types/node', + '@types/react', + 'react', + 'react-dom', + 'react-router-dom', + ]), + ], + [ + '@backstage/backend-app-api', + new Set([ + '@backstage/backend-common', + '@backstage/backend-plugin-api', + '@backstage/backend-tasks', + '@backstage/cli', + '@backstage/errors', + '@backstage/plugin-permission-node', + 'express', + 'express-promise-router', + 'winston', + ]), + ], + ]), + ); + }); + + it('for simple lockfile without dependencies', () => { + expect( + Lockfile.parse( + `${MODERN_HEADER} +"a@npm:^1": + version: "1.0.1" + +"b@npm:3": + version: "3.0.1" + +"b@npm:2.0.x": + version: "2.0.1" + checksum: sha512-abc2 +`, + ).createSimplifiedDependencyGraph(), + ).toEqual( + new Map([ + ['a', new Set()], + ['b', new Set()], + ]), + ); + }); + + it('for lockfile with dependencies', () => { + expect( + Lockfile.parse( + `${MODERN_HEADER} +"a@npm:^1": + version: "1.0.1" + dependencies: + b: "^2" + +"b@npm:3": + version: "3.0.1" + checksum: sha512-abc1 + +"b@npm:2.0.x": + version: "2.0.1" + checksum: sha512-abc2 + dependencies: + c: "^1" + +"b@npm:^2": + version: "2.0.0" + checksum: sha512-abc3 + peerDependencies: + d: "^1" + +"c@npm:^1": + version: "1.0.1" + +"d@npm:^1": + version: "1.0.2" +`, + ).createSimplifiedDependencyGraph(), + ).toEqual( + new Map([ + ['a', new Set(['b'])], + ['b', new Set(['c', 'd'])], + ['c', new Set()], + ['d', new Set()], + ]), + ); + }); + + it('for legacy lockfile', () => { + expect( + Lockfile.parse( + `${LEGACY_HEADER} +a@^1: + version "1.0.1" + dependencies: + b "^2" + +b@3: + version "3.0.1" + integrity sha512-abc1 + +b@2.0.x: + version "2.0.1" + integrity sha512-abc2 + dependencies: + c "^1" + +b@^2: + version "2.0.0" + integrity sha512-abc3 + dependencies: + d "^1" + +c@^1: + version "1.0.1" + integrity x + +d@^1: + version "1.0.1" + integrity x +`, + ).createSimplifiedDependencyGraph(), + ).toEqual( + new Map([ + ['a', new Set(['b'])], + ['b', new Set(['c', 'd'])], + ['c', new Set()], + ['d', new Set()], + ]), + ); + }); + }); }); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 016593ea8d..4eadaf55f9 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -18,6 +18,7 @@ import fs from 'fs-extra'; import semver from 'semver'; import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; +import { ExtendedPackage } from '../monorepo'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; @@ -25,14 +26,28 @@ type LockfileData = { [entry: string]: { version: string; resolved?: string; - integrity?: string; + integrity?: string /* old */; + checksum?: string /* new */; dependencies?: { [name: string]: string }; + peerDependencies?: { [name: string]: string }; }; }; type LockfileQueryEntry = { range: string; version: string; + dataKey: string; +}; + +type LockfileDiffEntry = { + name: string; + range: string; +}; + +type LockfileDiff = { + added: LockfileDiffEntry[]; + changed: LockfileDiffEntry[]; + removed: LockfileDiffEntry[]; }; /** Entries that have an invalid version range, for example an npm tag */ @@ -64,20 +79,6 @@ type AnalyzeResult = { newRanges: AnalyzeResultNewRange[]; }; -function parseLockfile(lockfileContents: string) { - try { - return { - object: parseSyml(lockfileContents), - type: 'success', - }; - } catch (err) { - return { - object: null, - type: err, - }; - } -} - // the new yarn header is handled out of band of the parsing // https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 const NEW_HEADER = `${[ @@ -85,11 +86,6 @@ const NEW_HEADER = `${[ `# Manual changes might be lost - proceed with caution!\n`, ].join(``)}\n`; -function stringifyLockfile(data: LockfileData, legacy: boolean) { - return legacy - ? legacyStringifyLockfile(data) - : NEW_HEADER + stringifySyml(data); -} // taken from yarn parser package // https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; @@ -110,13 +106,19 @@ const SPECIAL_OBJECT_KEYS = [ export class Lockfile { static async load(path: string) { const lockfileContents = await fs.readFile(path, 'utf8'); - const legacy = LEGACY_REGEX.test(lockfileContents); - const lockfile = parseLockfile(lockfileContents); - if (lockfile.type !== 'success') { - throw new Error(`Failed yarn.lock parse with ${lockfile.type}`); + return Lockfile.parse(lockfileContents); + } + + static parse(content: string) { + const legacy = LEGACY_REGEX.test(content); + + let data: LockfileData; + try { + data = parseSyml(content); + } catch (err) { + throw new Error(`Failed yarn.lock parse, ${err}`); } - const data = lockfile.object as LockfileData; const packages = new Map(); for (const [key, value] of Object.entries(data)) { @@ -139,15 +141,14 @@ export class Lockfile { if (range.startsWith('npm:')) { range = range.slice('npm:'.length); } - queries.push({ range, version: value.version }); + queries.push({ range, version: value.version, dataKey: key }); } } - return new Lockfile(path, packages, data, legacy); + return new Lockfile(packages, data, legacy); } private constructor( - private readonly path: string, private readonly packages: Map, private readonly data: LockfileData, private readonly legacy: boolean = false, @@ -164,8 +165,11 @@ export class Lockfile { } /** Analyzes the lockfile to identify possible actions and warnings for the entries */ - analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult { - const { filter } = options ?? {}; + analyze(options: { + filter?: (name: string) => boolean; + localPackages: Map; + }): AnalyzeResult { + const { filter, localPackages } = options; const result: AnalyzeResult = { invalidRanges: [], newVersions: [], @@ -178,7 +182,9 @@ export class Lockfile { } // Get rid of and signal any invalid ranges upfront - const invalid = allEntries.filter(e => !semver.validRange(e.range)); + const invalid = allEntries.filter( + e => !semver.validRange(e.range) && !e.range.startsWith('workspace:'), + ); result.invalidRanges.push( ...invalid.map(({ range }) => ({ name, range })), ); @@ -190,36 +196,55 @@ export class Lockfile { } // Find all versions currently in use - const versions = Array.from(new Set(entries.map(e => e.version))).sort( - (v1, v2) => semver.rcompare(v1, v2), - ); + const versions = Array.from(new Set(entries.map(e => e.version))) + .map(v => { + // Translate workspace:^ references to the actual version + if (v === '0.0.0-use.local') { + const local = localPackages.get(name); + if (!local) { + throw new Error(`No local package found for ${name}`); + } + if (!local.packageJson.version) { + throw new Error(`No version found for local package ${name}`); + } + return { + entryVersion: v, + actualVersion: local.packageJson.version, + }; + } + return { entryVersion: v, actualVersion: v }; + }) + .sort((v1, v2) => semver.rcompare(v1.actualVersion, v2.actualVersion)); // If we're not using at least 2 different versions we're done if (versions.length < 2) { continue; } + // TODO(Rugvip): Support bumping into workspace ranges too const acceptedVersions = new Set(); for (const { version, range } of entries) { // Finds the highest matching version from the the known versions // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one - const acceptedVersion = versions.find(v => semver.satisfies(v, range)); + const acceptedVersion = versions.find(v => + semver.satisfies(v.actualVersion, range), + ); if (!acceptedVersion) { throw new Error( `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, ); } - if (acceptedVersion !== version) { + if (acceptedVersion.entryVersion !== version) { result.newVersions.push({ name, range, - newVersion: acceptedVersion, + newVersion: acceptedVersion.entryVersion, oldVersion: version, }); } - acceptedVersions.add(acceptedVersion); + acceptedVersions.add(acceptedVersion.actualVersion); } // If all ranges were able to accept the same version, we're done @@ -315,11 +340,91 @@ export class Lockfile { } } - async save() { - await fs.writeFile(this.path, this.toString(), 'utf8'); + createSimplifiedDependencyGraph(): Map> { + const graph = new Map>(); + + for (const [name, entries] of this.packages) { + const dependencies = new Set( + entries.flatMap(e => { + const data = this.data[e.dataKey]; + return [ + ...Object.keys(data?.dependencies ?? {}), + ...Object.keys(data?.peerDependencies ?? {}), + ]; + }), + ); + graph.set(name, dependencies); + } + + return graph; + } + + /** + * Diff with another lockfile, returning entries that have been + * added, changed, and removed compared to the other lockfile. + */ + diff(otherLockfile: Lockfile): LockfileDiff { + const diff = { + added: new Array<{ name: string; range: string }>(), + changed: new Array<{ name: string; range: string }>(), + removed: new Array<{ name: string; range: string }>(), + }; + + // Keeps track of packages that only exist in this lockfile + const remainingOldNames = new Set(this.packages.keys()); + + for (const [name, otherQueries] of otherLockfile.packages) { + remainingOldNames.delete(name); + + const thisQueries = this.packages.get(name); + // If the packages doesn't exist in this lockfile, add all entries + if (!thisQueries) { + diff.removed.push(...otherQueries.map(q => ({ name, range: q.range }))); + continue; + } + + const remainingOldRanges = new Set(thisQueries.map(q => q.range)); + + for (const otherQuery of otherQueries) { + remainingOldRanges.delete(otherQuery.range); + + const thisQuery = thisQueries.find(q => q.range === otherQuery.range); + if (!thisQuery) { + diff.removed.push({ name, range: otherQuery.range }); + continue; + } + + const otherPkg = otherLockfile.data[otherQuery.dataKey]; + const thisPkg = this.data[thisQuery.dataKey]; + if (otherPkg && thisPkg) { + const thisCheck = thisPkg.integrity || thisPkg.checksum; + const otherCheck = otherPkg.integrity || otherPkg.checksum; + if (thisCheck !== otherCheck) { + diff.changed.push({ name, range: otherQuery.range }); + } + } + } + + for (const thisRange of remainingOldRanges) { + diff.added.push({ name, range: thisRange }); + } + } + + for (const name of remainingOldNames) { + const queries = this.packages.get(name) ?? []; + diff.added.push(...queries.map(q => ({ name, range: q.range }))); + } + + return diff; + } + + async save(path: string) { + await fs.writeFile(path, this.toString(), 'utf8'); } toString() { - return stringifyLockfile(this.data, this.legacy); + return this.legacy + ? legacyStringifyLockfile(this.data) + : NEW_HEADER + stringifySyml(this.data); } } diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 09d5fe03f8..2d0c1d240b 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -35,9 +35,6 @@ "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.17.11", "@microsoft/api-extractor": "^7.23.0", - "@microsoft/api-extractor-model": "^7.17.2", - "@microsoft/tsdoc": "0.14.1", - "@microsoft/tsdoc-config": "0.16.2", "chalk": "^4.0.0", "commander": "^9.1.0", "fs-extra": "10.1.0", @@ -53,6 +50,9 @@ "mock-fs": "^5.1.0" }, "peerDependencies": { + "@microsoft/api-extractor-model": "*", + "@microsoft/tsdoc": "*", + "@microsoft/tsdoc-config": "*", "@rushstack/node-core-library": "*", "prettier": "^2.8.1", "typescript": "> 3.0.0" diff --git a/packages/repo-tools/src/lib/paths.test.ts b/packages/repo-tools/src/lib/paths.test.ts index c642900612..5d8dc6ce18 100644 --- a/packages/repo-tools/src/lib/paths.test.ts +++ b/packages/repo-tools/src/lib/paths.test.ts @@ -15,7 +15,7 @@ */ import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import { resolvePackagePath, paths, findPackageDirs } from './paths'; describe('paths', () => { @@ -61,15 +61,15 @@ describe('paths', () => { }); it('should return the path to the package if it exists and has a package.json', async () => { expect(await resolvePackagePath('packages/package-a')).toBe( - 'packages/package-a', + joinPath('packages', 'package-a'), ); expect(await resolvePackagePath('packages/package-b')).toBe( - 'packages/package-b', + joinPath('packages', 'package-b'), ); }); it('should work with absolute paths', async () => { expect(await resolvePackagePath('/root/packages/package-a')).toBe( - 'packages/package-a', + joinPath('packages', 'package-a'), ); }); it('should return undefined if the pat is not a directory', async () => { @@ -79,19 +79,19 @@ describe('paths', () => { describe('findPackageDirs', () => { it('should return only the given packages', async () => { expect(await findPackageDirs(['packages/package-a'])).toEqual([ - 'packages/package-a', + joinPath('packages', 'package-a'), ]); }); it('should return only the given packages when using glob patterns', async () => { expect(await findPackageDirs(['packages/*'])).toEqual([ - 'packages/package-a', - 'packages/package-b', + joinPath('packages', 'package-a'), + joinPath('packages', 'package-b'), ]); expect(await findPackageDirs(['packages/*', 'plugins/*'])).toEqual([ - 'packages/package-a', - 'packages/package-b', - 'plugins/plugin-a', - 'plugins/plugin-b', + joinPath('packages', 'package-a'), + joinPath('packages', 'package-b'), + joinPath('plugins', 'plugin-a'), + joinPath('plugins', 'plugin-b'), ]); }); it('should return only the given packages when using absolute paths', async () => { @@ -100,15 +100,18 @@ describe('paths', () => { '/root/packages/package-a', '/root/plugins/plugin-b', ]), - ).toEqual(['packages/package-a', 'plugins/plugin-b']); + ).toEqual([ + joinPath('packages', 'package-a'), + joinPath('plugins', 'plugin-b'), + ]); }); it('should return only the given packages when using absolute paths with glob patterns', async () => { expect( await findPackageDirs(['/root/packages/*', '/root/plugins/*-a']), ).toEqual([ - 'packages/package-a', - 'packages/package-b', - 'plugins/plugin-a', + joinPath('packages', 'package-a'), + joinPath('packages', 'package-b'), + joinPath('plugins', 'plugin-a'), ]); }); }); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 17372fa42b..b194c11671 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -11,6 +11,7 @@ ### Patch Changes +- 2cb9998: Fixed bug in `EntityOwnerPicker` and `EntityLifecyclePicker` that filtered on unavailable tags for the selected kind. - 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`. - 6ffa47bb0a: Cleanup and small fixes for the kind selector - 19356df560: Updated dependency `zen-observable` to `^0.9.0`. diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 7da357d806..de7bbb89be 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -202,4 +202,33 @@ describe('', () => { tags: new EntityTagFilter(['tag2']), }); }); + it('removes tags from filters if there are none available', async () => { + const updateFilters = jest.fn(); + const mockCatalogApiRefNoTags = { + getEntityFacets: async () => ({ + facets: { + 'metadata.tags': {}, + }, + }), + } as unknown as CatalogApi; + + render( + + + + + , + , + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: undefined, + }), + ); + }); }); diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index 25f02e8adc..4a1dc03e96 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -93,10 +93,14 @@ export const EntityTagPicker = (props: EntityTagPickerProps) => { }, [queryParamTags]); useEffect(() => { + const tags = Object.keys(availableTags ?? {}); updateFilters({ - tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined, + tags: + selectedTags.length && tags.length + ? new EntityTagFilter(selectedTags) + : undefined, }); - }, [selectedTags, updateFilters]); + }, [selectedTags, updateFilters, availableTags]); if (!Object.keys(availableTags ?? {}).length) return null; diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index c33874064b..440af50714 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-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 { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -15,6 +16,8 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + catalogApi?: CatalogApi; // (undocumented) config: Config; // (undocumented) diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts index e0b4c6589e..44b1411cc4 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.test.ts @@ -209,7 +209,7 @@ describe('CodeCoverageUtils', () => { } catch (error: any) { err = error; } - expect(err?.message).toEqual('Content-Type missing'); + expect(err?.message).toEqual('Content-Type header missing'); }); it('rejects unsupported content type', () => { @@ -223,7 +223,9 @@ describe('CodeCoverageUtils', () => { } catch (error: any) { err = error; } - expect(err?.message).toEqual('Illegal Content-Type'); + expect(err?.message).toEqual( + 'Content-Type header "application/json" not supported, expected "text/xml" possibly followed by a charset', + ); }); it('parses the body', () => { diff --git a/plugins/code-coverage-backend/src/service/CoverageUtils.ts b/plugins/code-coverage-backend/src/service/CoverageUtils.ts index d965ae6300..a4d3405868 100644 --- a/plugins/code-coverage-backend/src/service/CoverageUtils.ts +++ b/plugins/code-coverage-backend/src/service/CoverageUtils.ts @@ -162,9 +162,11 @@ export class CoverageUtils { validateRequestBody(req: Request) { const contentType = req.headers['content-type']; if (!contentType) { - throw new InputError('Content-Type missing'); + throw new InputError('Content-Type header missing'); } else if (!contentType.match(/^text\/xml($|;)/)) { - throw new InputError('Illegal Content-Type'); + throw new InputError( + `Content-Type header "${contentType}" not supported, expected "text/xml" possibly followed by a charset`, + ); } const body = req.body; if (!body) { diff --git a/plugins/code-coverage-backend/src/service/converter/jacoco.ts b/plugins/code-coverage-backend/src/service/converter/jacoco.ts index f3047d9f00..ac5ccbfa45 100644 --- a/plugins/code-coverage-backend/src/service/converter/jacoco.ts +++ b/plugins/code-coverage-backend/src/service/converter/jacoco.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { BranchHit, FileEntry } from '../types'; import { JacocoSourceFile, JacocoXML } from './types'; import { Logger } from 'winston'; diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index b87b21bffd..4d4fb779b5 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -46,6 +46,7 @@ export interface RouterOptions { database: PluginDatabaseManager; urlReader: UrlReader; logger: Logger; + catalogApi?: CatalogApi; } export interface CodeCoverageApi { @@ -59,7 +60,8 @@ export const makeRouter = async ( const codeCoverageDatabase = await CodeCoverageDatabase.create(database); const codecovUrl = await discovery.getExternalBaseUrl('code-coverage'); - const catalogApi: CatalogApi = new CatalogClient({ discoveryApi: discovery }); + const catalogApi = + options.catalogApi ?? new CatalogClient({ discoveryApi: discovery }); const scm = ScmIntegrations.fromConfig(config); const router = Router(); @@ -167,10 +169,10 @@ export const makeRouter = async ( * /report?entity=component:default/mycomponent&coverageType=cobertura */ router.post('/report', async (req, res) => { - const { entity, coverageType } = req.query; - const entityLookup = await catalogApi.getEntityByRef(entity as string); - if (!entityLookup) { - throw new NotFoundError(`No entity found matching ${entity}`); + const { entity: entityRef, coverageType } = req.query; + const entity = await catalogApi.getEntityByRef(entityRef as string); + if (!entity) { + throw new NotFoundError(`No entity found matching ${entityRef}`); } let converter: Converter; @@ -185,7 +187,7 @@ export const makeRouter = async ( } const { sourceLocation, vcs, scmFiles, body } = - await utils.processCoveragePayload(entityLookup, req); + await utils.processCoveragePayload(entity, req); const files = converter.convert(body, scmFiles); if (!files || files.length === 0) { @@ -193,7 +195,7 @@ export const makeRouter = async ( } const coverage = await utils.buildCoverage( - entityLookup, + entity, sourceLocation, vcs, files, @@ -204,7 +206,7 @@ export const makeRouter = async ( links: [ { rel: 'coverage', - href: `${codecovUrl}/report?entity=${entity}`, + href: `${codecovUrl}/report?entity=${entityRef}`, }, ], }); diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts index c4ced6baeb..41493660fb 100644 --- a/plugins/code-coverage-backend/src/service/standaloneServer.ts +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -21,6 +21,8 @@ import { UrlReaders, useHotMemoize, } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model'; import { Server } from 'http'; import knexFactory from 'knex'; import { Logger } from 'winston'; @@ -52,13 +54,27 @@ export async function startStandaloneServer( return knex; }); + const catalogApi = { + async getEntityByRef(entityRef: string | CompoundEntityRef) { + const { kind, namespace, name } = parseEntityRef(entityRef); + return { + apiVersion: 'backstage.io/v1alpha1', + kind, + metadata: { name, namespace }, + spec: {}, + }; + }, + } as Partial as CatalogApi; + logger.debug('Starting application server...'); + const router = await createRouter({ database: { getClient: async () => db }, config, discovery: SingleHostDiscovery.fromConfig(config), urlReader: UrlReaders.default({ logger, config }), logger, + catalogApi, }); let service = createServiceBuilder(module) diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 674ef1e43f..fd1d5ba5fb 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -44,8 +44,10 @@ import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: BackstageTheme) => ({ graph: { minHeight: '100%', + flex: 1, }, graphWrapper: { + display: 'flex', height: '100%', }, organizationNode: { diff --git a/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx b/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx index 30a83e4cec..2597986031 100644 --- a/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx +++ b/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx @@ -86,7 +86,7 @@ export const ServicesTable = ({ cellStyle: smColumnStyle, headerStyle: smColumnStyle, render: rowData => ( - {rowData.uptime.uptimePercentage.p90} + {rowData.uptime?.uptimePercentage?.p90 || ''} ), }; const actionsColumn: TableColumn = { diff --git a/plugins/scaffolder-backend-module-cookiecutter/README.md b/plugins/scaffolder-backend-module-cookiecutter/README.md index 8516139e64..ae34e09c96 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/README.md +++ b/plugins/scaffolder-backend-module-cookiecutter/README.md @@ -73,16 +73,16 @@ spec: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group system: title: System type: string description: System of the component ui:field: EntityPicker ui:options: - allowedKinds: - - System + catalogFilter: + kind: System defaultKind: System - title: Choose a location diff --git a/plugins/scaffolder-backend-module-rails/README.md b/plugins/scaffolder-backend-module-rails/README.md index 738cd43cff..f4ec086775 100644 --- a/plugins/scaffolder-backend-module-rails/README.md +++ b/plugins/scaffolder-backend-module-rails/README.md @@ -74,16 +74,16 @@ spec: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group system: title: System type: string description: System of the component ui:field: EntityPicker ui:options: - allowedKinds: - - System + catalogFilter: + kind: System defaultKind: System - title: Choose a location diff --git a/plugins/scaffolder-backend-module-yeoman/README.md b/plugins/scaffolder-backend-module-yeoman/README.md index 5242ef298c..b5c7b4d026 100644 --- a/plugins/scaffolder-backend-module-yeoman/README.md +++ b/plugins/scaffolder-backend-module-yeoman/README.md @@ -73,16 +73,16 @@ spec: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group system: title: System type: string description: System of the component ui:field: EntityPicker ui:options: - allowedKinds: - - System + catalogFilter: + kind: System defaultKind: System - title: Choose a location diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml index 58955582d2..0a7c841d8f 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml @@ -49,8 +49,8 @@ spec: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group steps: - id: fetch-base diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 6a029af6bf..1b1cfb80bc 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -105,6 +105,10 @@ export const EntityPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -116,6 +120,10 @@ export const EntityPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -326,6 +334,10 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; @@ -336,6 +348,10 @@ export const OwnerPickerFieldSchema: FieldSchema< defaultNamespace?: string | false | undefined; allowedKinds?: string[] | undefined; allowArbitraryValues?: boolean | undefined; + catalogFilter?: + | Record + | Record[] + | undefined; } >; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx index 041b7797ee..5bc32ed581 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -53,8 +53,8 @@ parameters: description: Owner of the component ui:field: OwnerPicker ui:options: - allowedKinds: - - Group + catalogFilter: + kind: Group - title: Choose a location required: - repoUrl diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 942eef9d82..b6d6688a09 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { EntityFilterQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -34,7 +35,13 @@ describe('', () => { const schema = {}; const required = false; let uiSchema: { - 'ui:options': { allowedKinds?: string[]; defaultKind?: string }; + 'ui:options': { + allowedKinds?: string[]; + defaultKind?: string; + allowArbitraryValues?: boolean; + defaultNamespace?: string | false; + catalogFilter?: EntityFilterQuery; + }; }; const rawErrors: string[] = []; const formData = undefined; @@ -66,7 +73,7 @@ describe('', () => { afterEach(() => jest.resetAllMocks()); - describe('without allowedKinds', () => { + describe('without allowedKinds and catalogFilter', () => { beforeEach(() => { uiSchema = { 'ui:options': {} }; props = { @@ -136,4 +143,97 @@ describe('', () => { }); }); }); + + describe('with catalogFilter', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + catalogFilter: [ + { + kind: ['Group'], + 'metadata.name': 'test-entity', + }, + { + kind: ['User'], + 'metadata.name': 'test-entity', + }, + ], + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('searches for a specific group entity', async () => { + await renderInTestApp( + + + , + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: [ + { + kind: ['Group'], + 'metadata.name': 'test-entity', + }, + { + kind: ['User'], + 'metadata.name': 'test-entity', + }, + ], + }); + }); + }); + + describe('catalogFilter should take precedence over allowedKinds', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + catalogFilter: [ + { + kind: ['Group'], + 'metadata.name': 'test-group', + }, + ], + allowedKinds: ['User'], + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('searches for a Group entity', async () => { + await renderInTestApp( + + + , + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: [ + { + kind: ['Group'], + 'metadata.name': 'test-group', + }, + ], + }); + }); + }); }); diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index bc78e00576..48e60b60ad 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { type EntityFilterQuery } from '@backstage/catalog-client'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, @@ -44,6 +45,11 @@ export const EntityPicker = (props: EntityPickerProps) => { idSchema, } = props; const allowedKinds = uiSchema['ui:options']?.allowedKinds; + + const catalogFilter: EntityFilterQuery | undefined = + uiSchema['ui:options']?.catalogFilter || + (allowedKinds && { kind: allowedKinds }); + const defaultKind = uiSchema['ui:options']?.defaultKind; const defaultNamespace = uiSchema['ui:options']?.defaultNamespace; @@ -51,7 +57,7 @@ export const EntityPicker = (props: EntityPickerProps) => { const { value: entities, loading } = useAsync(() => catalogApi.getEntities( - allowedKinds ? { filter: { kind: allowedKinds } } : undefined, + catalogFilter ? { filter: catalogFilter } : undefined, ), ); diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 9236457fc0..f83dfbf817 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -16,16 +16,28 @@ import { z } from 'zod'; import { makeFieldSchemaFromZod } from '../utils'; +/** + * @public + */ +export const entityQueryFilterExpressionSchema = z.record( + z.string().or(z.array(z.string())), +); + /** * @public */ export const EntityPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ + /** + * @deprecated Use `catalogFilter` instead. + */ allowedKinds: z .array(z.string()) .optional() - .describe('List of kinds of entities to derive options from'), + .describe( + 'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from', + ), defaultKind: z .string() .optional() @@ -42,6 +54,11 @@ export const EntityPickerFieldSchema = makeFieldSchemaFromZod( .describe( 'The default namespace. Options with this namespace will not be prefixed.', ), + catalogFilter: z + .array(entityQueryFilterExpressionSchema) + .or(entityQueryFilterExpressionSchema) + .optional() + .describe('List of key-value filter expression for entities'), }), ); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index b78d3add9e..00508593f5 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { type EntityFilterQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -32,7 +33,15 @@ describe('', () => { const onChange = jest.fn(); const schema = {}; const required = false; - let uiSchema: { 'ui:options': { allowedKinds?: string[] } }; + let uiSchema: { + 'ui:options': { + allowedKinds?: string[]; + defaultKind?: string; + allowArbitraryValues?: boolean; + defaultNamespace?: string | false; + catalogFilter?: EntityFilterQuery; + }; + }; const rawErrors: string[] = []; const formData = undefined; @@ -63,7 +72,7 @@ describe('', () => { afterEach(() => jest.resetAllMocks()); - describe('without allowedKinds', () => { + describe('without catalogFilter and allowedKinds', () => { beforeEach(() => { uiSchema = { 'ui:options': {} }; props = { @@ -108,7 +117,7 @@ describe('', () => { catalogApi.getEntities.mockResolvedValue({ items: entities }); }); - it('searches for users and groups', async () => { + it('searches for users', async () => { await renderInTestApp( @@ -122,4 +131,93 @@ describe('', () => { }); }); }); + + describe('with catalogFilter', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + catalogFilter: [ + { + kind: ['Group'], + 'spec.type': 'team', + }, + ], + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('searches for group entities of type team', async () => { + await renderInTestApp( + + + , + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: [ + { + kind: ['Group'], + 'spec.type': 'team', + }, + ], + }); + }); + }); + + describe('catalogFilter should take precedence over allowedKinds', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + allowedKinds: ['User'], + catalogFilter: [ + { + kind: ['Group', 'User'], + }, + { + 'spec.type': ['team', 'business-unit'], + }, + ], + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('searches for users and groups or teams and business units', async () => { + await renderInTestApp( + + + , + ); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: [ + { + kind: ['Group', 'User'], + }, + { + 'spec.type': ['team', 'business-unit'], + }, + ], + }); + }); + }); }); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 1c4c356906..5c69e73e12 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -33,14 +33,16 @@ export const OwnerPicker = (props: OwnerPickerProps) => { } = props; const defaultNamespace = uiSchema['ui:options']?.defaultNamespace; + const allowedKinds = uiSchema['ui:options']?.allowedKinds; + + const catalogFilter = uiSchema['ui:options']?.catalogFilter || { + kind: allowedKinds || ['Group', 'User'], + }; const ownerUiSchema = { ...uiSchema, 'ui:options': { - allowedKinds: (uiSchema['ui:options']?.allowedKinds || [ - 'Group', - 'User', - ]) as string[], + catalogFilter, defaultKind: 'Group', allowArbitraryValues: uiSchema['ui:options']?.allowArbitraryValues ?? true, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts index 56edef1343..0dbe2fa74f 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -15,6 +15,7 @@ */ import { z } from 'zod'; import { makeFieldSchemaFromZod } from '../utils'; +import { entityQueryFilterExpressionSchema } from '../EntityPicker/schema'; /** * @public @@ -22,12 +23,15 @@ import { makeFieldSchemaFromZod } from '../utils'; export const OwnerPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ + /** + * @deprecated Use `catalogFilter` instead. + */ allowedKinds: z .array(z.string()) .default(['Group', 'User']) .optional() .describe( - 'List of kinds of entities to derive options from. Defaults to Group and User', + 'DEPRECATED: Use `catalogFilter` instead. List of kinds of entities to derive options from. Defaults to Group and User', ), allowArbitraryValues: z .boolean() @@ -39,6 +43,11 @@ export const OwnerPickerFieldSchema = makeFieldSchemaFromZod( .describe( 'The default namespace. Options with this namespace will not be prefixed.', ), + catalogFilter: z + .array(entityQueryFilterExpressionSchema) + .or(entityQueryFilterExpressionSchema) + .optional() + .describe('List of key-value filter expression for entities'), }), ); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index af86abf1c6..2660eed47d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -20,6 +20,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import type { RJSFValidationError } from '@rjsf/utils'; import { JsonValue } from '@backstage/types'; +import { NextFieldExtensionComponentProps } from '../../../extensions/types'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { @@ -110,6 +111,98 @@ describe('Stepper', () => { ); }); + it('should merge nested formData correctly in multiple steps', async () => { + const Repo = ({ + onChange, + }: NextFieldExtensionComponentProps<{ repository: string }, any>) => ( + onChange({ repository: e.target.value })} + defaultValue="" + /> + ); + + const Owner = ({ + onChange, + }: NextFieldExtensionComponentProps<{ owner: string }, any>) => ( + onChange({ owner: e.target.value })} + defaultValue="" + /> + ); + + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + first: { + type: 'object', + 'ui:field': 'Repo', + }, + }, + }, + }, + { + title: 'Step 2', + schema: { + properties: { + second: { + type: 'object', + 'ui:field': 'Owner', + }, + }, + }, + }, + ], + title: 'React JSON Schema Form Test', + }; + + const onComplete = jest.fn(async (values: Record) => { + expect(values).toEqual({ + first: { repository: 'Repo' }, + second: { owner: 'Owner' }, + }); + }); + + const { getByRole } = await renderInTestApp( + , + ); + + await fireEvent.change(getByRole('textbox', { name: 'repo' }), { + target: { value: 'Repo' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Next' })); + }); + + await fireEvent.change(getByRole('textbox', { name: 'owner' }), { + target: { value: 'Owner' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Create' })); + }); + + expect(onComplete).toHaveBeenCalled(); + }); + it('should render custom field extensions properly', async () => { const MockComponent = () => { return

im a custom field extension

; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 3d5cb1bfd2..d4d57eed22 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -26,9 +26,9 @@ import { Button, makeStyles, } from '@material-ui/core'; -import { withTheme } from '@rjsf/core-v5'; +import { type IChangeEvent, withTheme } from '@rjsf/core-v5'; import { ErrorSchema, FieldValidation } from '@rjsf/utils'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { NextFieldExtensionOptions } from '../../../extensions'; import { TemplateParameterSchema } from '../../../types'; import { createAsyncValidators } from './createAsyncValidators'; @@ -36,7 +36,6 @@ import { useTemplateSchema } from './useTemplateSchema'; import { ReviewState } from './ReviewState'; import validator from '@rjsf/validator-ajv6'; import { selectedTemplateRouteRef } from '../../../routes'; -import { getDefaultFormState } from '@rjsf/utils'; import { useFormData } from './useFormData'; import { FormProps } from '../../types'; @@ -102,6 +101,12 @@ export const Stepper = (props: StepperProps) => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; + const handleChange = useCallback( + (e: IChangeEvent) => + setFormState(current => ({ ...current, ...e.formData })), + [setFormState], + ); + const handleNext = async ({ formData, }: { @@ -111,18 +116,7 @@ export const Stepper = (props: StepperProps) => { // to display it's own loading? Or should we grey out the entire form. setErrors(undefined); - const schema = steps[activeStep]?.schema; - const rootSchema = steps[activeStep]?.mergedSchema; - - const newFormData = getDefaultFormState( - validator, - schema, - formData, - rootSchema, - true, - ); - - const returnedValidation = await validation(newFormData); + const returnedValidation = await validation(formData); const hasErrors = Object.values(returnedValidation).some( i => i.__errors?.length, @@ -138,7 +132,7 @@ export const Stepper = (props: StepperProps) => { return stepNum; }); } - setFormState(current => ({ ...current, ...newFormData })); + setFormState(current => ({ ...current, ...formData })); }; return ( @@ -165,6 +159,7 @@ export const Stepper = (props: StepperProps) => { onSubmit={handleNext} fields={extensions} showErrorList={false} + onChange={handleChange} {...(props.FormProps ?? {})} >
diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts index 92193976b4..94b59fcf42 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -250,4 +250,33 @@ describe('ElasticSearchSearchEngineIndexer', () => { // Final deletion shouldn't be called. expect(deleteSpy).not.toHaveBeenCalled(); }); + + it('handles bulk client rejection', async () => { + // Given an ES client wrapper that rejects an error + const expectedError = new Error('HTTP Timeout'); + const mockClientWrapper = ElasticSearchClientWrapper.fromClientOptions({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), + }); + mockClientWrapper.bulk = jest.fn().mockRejectedValue(expectedError); + + // And a search engine indexer that uses that client wrapper + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: getVoidLogger(), + elasticSearchClientWrapper: mockClientWrapper, + batchSize: 1000, + }); + + // When the indexer is run in the test pipeline + const { error } = await TestPipeline.fromIndexer(indexer) + .withDocuments([{ title: 'a', location: 'a', text: '/a' }]) + .execute(); + + // Then the pipeline should have received the expected error + expect(error).toBe(expectedError); + }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index 29f20ea650..a9d15c421e 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -45,7 +45,6 @@ function duration(startTimestamp: [number, number]): string { * @public */ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { - private received: number = 0; private processed: number = 0; private removableIndices: string[] = []; @@ -59,10 +58,13 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { private readonly logger: Logger; private readonly sourceStream: Readable; private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper; + private configuredBatchSize: number; private bulkResult: Promise; + private bulkClientError?: Error; constructor(options: ElasticSearchSearchEngineIndexerOptions) { super({ batchSize: options.batchSize }); + this.configuredBatchSize = options.batchSize; this.logger = options.logger.child({ documentType: options.type }); this.startTimestamp = process.hrtime(); this.type = options.type; @@ -94,6 +96,11 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { }, refreshOnCompletion: that.indexName, }); + + // Safely catch errors thrown by the bulk helper client, e.g. HTTP timeouts + this.bulkResult.catch(e => { + this.bulkClientError = e; + }); } async initialize(): Promise { @@ -115,7 +122,6 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { async index(documents: IndexableDocument[]): Promise { await this.isReady(); documents.forEach(document => { - this.received++; this.sourceStream.push(document); }); } @@ -197,11 +203,41 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { * backpressure in other parts of the indexing pipeline. */ private isReady(): Promise { - return new Promise(resolve => { + // Early exit if the underlying ES client encountered an error. + if (this.bulkClientError) { + return Promise.reject(this.bulkClientError); + } + + // Optimization: if the stream that ES reads from has fewer docs queued + // than the configured batch size, continue early to allow more docs to be + // queued + if (this.sourceStream.readableLength < this.configuredBatchSize) { + return Promise.resolve(); + } + + // Otherwise, continue periodically checking the stream queue to see if + // ES has consumed the documents and continue when it's ready for more. + return new Promise((isReady, abort) => { + let streamLengthChecks = 0; const interval = setInterval(() => { - if (this.received === this.processed) { + streamLengthChecks++; + + if (this.sourceStream.readableLength < this.configuredBatchSize) { clearInterval(interval); - resolve(); + isReady(); + } + + // Do not allow this interval to loop endlessly; anything longer than 5 + // minutes likely indicates an unrecoverable error in ES; direct the + // user to inspect ES logs for more clues and abort in order to allow + // the index to be cleaned up. + if (streamLengthChecks >= 6000) { + clearInterval(interval); + abort( + new Error( + 'Exceeded 5 minutes waiting for elastic to be ready to accept more documents. Check the elastic logs for possible problems.', + ), + ); } }, 50); }); diff --git a/yarn.lock b/yarn.lock index e45abbd492..b7e826b276 100644 --- a/yarn.lock +++ b/yarn.lock @@ -408,15 +408,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.235.0" +"@aws-sdk/client-cognito-identity@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.236.0" dependencies: "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.235.0 + "@aws-sdk/client-sts": 3.236.0 "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.235.0 + "@aws-sdk/credential-provider-node": 3.236.0 "@aws-sdk/fetch-http-handler": 3.226.0 "@aws-sdk/hash-node": 3.226.0 "@aws-sdk/invalid-dependency": 3.226.0 @@ -448,20 +448,20 @@ __metadata: "@aws-sdk/util-utf8-browser": 3.188.0 "@aws-sdk/util-utf8-node": 3.208.0 tslib: ^2.3.1 - checksum: 4637132cc1a95823867c5ee80b35c7b84ddeed83c0778c2d7c197b26f24899641c825926c886c293f36d62e6b72be03ee091ac11cc9dbd875ad85936548ee178 + checksum: 808339bd0113f65674dea0d65af25c350ff5a8f2595f9dd11215b6e457df5d243db156042dc4bfbb39b302d6f871a71fbbed20f403207fb6e4c9e444eb802bf1 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/client-s3@npm:3.235.0" + version: 3.236.0 + resolution: "@aws-sdk/client-s3@npm:3.236.0" dependencies: "@aws-crypto/sha1-browser": 2.0.0 "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.235.0 + "@aws-sdk/client-sts": 3.236.0 "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.235.0 + "@aws-sdk/credential-provider-node": 3.236.0 "@aws-sdk/eventstream-serde-browser": 3.226.0 "@aws-sdk/eventstream-serde-config-resolver": 3.226.0 "@aws-sdk/eventstream-serde-node": 3.226.0 @@ -511,19 +511,19 @@ __metadata: "@aws-sdk/xml-builder": 3.201.0 fast-xml-parser: 4.0.11 tslib: ^2.3.1 - checksum: b16ce0a34a4d302edbc9cae91296a90a907511d6f0985feccf2196d537e1f1bfff6bfc0dafcb7ea1d5d7d0afd79d843cc15f8a5f87debd08852de338c698169b + checksum: 6a8953c229a1824bf1639dfa1a593bff3419c2c338da8df956d5cd3098dc3c1236ee63564347a22f40dc5947322b3f315ae7e39dd9c115ccf8a28199168c6d3b languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/client-sqs@npm:3.235.0" + version: 3.236.0 + resolution: "@aws-sdk/client-sqs@npm:3.236.0" dependencies: "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 - "@aws-sdk/client-sts": 3.235.0 + "@aws-sdk/client-sts": 3.236.0 "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.235.0 + "@aws-sdk/credential-provider-node": 3.236.0 "@aws-sdk/fetch-http-handler": 3.226.0 "@aws-sdk/hash-node": 3.226.0 "@aws-sdk/invalid-dependency": 3.226.0 @@ -558,13 +558,13 @@ __metadata: "@aws-sdk/util-utf8-node": 3.208.0 fast-xml-parser: 4.0.11 tslib: ^2.3.1 - checksum: 04da2ab0a850fa74265b9ab91b14fdf470d7f2850e31866b3582b83a01991bd9555b370e34bceba2445910b2d60cf8e17d7ead1f5caa94987e4114db949548f8 + checksum: c1d3eb3886d8d799afd081cf76822627e359495daa94edb615a001704933b31d276249828c5e5823af49ba08826875a4c768d8e6b4493abe9b65583a585839b7 languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.235.0" +"@aws-sdk/client-sso-oidc@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.236.0" dependencies: "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 @@ -599,13 +599,13 @@ __metadata: "@aws-sdk/util-utf8-browser": 3.188.0 "@aws-sdk/util-utf8-node": 3.208.0 tslib: ^2.3.1 - checksum: c62558375fb6d3ce44136019de99e5134805894a80cec191e145003c0e065eb52ba0a288a62884371d6a69370f92df522b120e1c5428520c09ba0f4cdc5b91b7 + checksum: 6fbbebb37547bafc9345c1fd03692728d6b6015f7583f22b2542beaac7f395ab29af884f1a353e78dd147ebf8b109b1d6678f9d1ae18c2b70739e9ceee6781ef languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/client-sso@npm:3.235.0" +"@aws-sdk/client-sso@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/client-sso@npm:3.236.0" dependencies: "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 @@ -640,18 +640,18 @@ __metadata: "@aws-sdk/util-utf8-browser": 3.188.0 "@aws-sdk/util-utf8-node": 3.208.0 tslib: ^2.3.1 - checksum: 9852704cc383cbc8e44013eed6de514f60dc4f70ab89ceea79971b4b286db6b923648e8e4169d248f0888efed19c7c5fcc5748177577635d07080fd4980431d7 + checksum: 15cb25a3c12fe0f11ed320c8f02f72ee0d43291a14c06fce81f608fbc75a85785698fbffc3112e7e92ece25460d24670ba345535b43003b20798a0fcc8a7b31d languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.235.0, @aws-sdk/client-sts@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/client-sts@npm:3.235.0" +"@aws-sdk/client-sts@npm:3.236.0, @aws-sdk/client-sts@npm:^3.208.0": + version: 3.236.0 + resolution: "@aws-sdk/client-sts@npm:3.236.0" dependencies: "@aws-crypto/sha256-browser": 2.0.0 "@aws-crypto/sha256-js": 2.0.0 "@aws-sdk/config-resolver": 3.234.0 - "@aws-sdk/credential-provider-node": 3.235.0 + "@aws-sdk/credential-provider-node": 3.236.0 "@aws-sdk/fetch-http-handler": 3.226.0 "@aws-sdk/hash-node": 3.226.0 "@aws-sdk/invalid-dependency": 3.226.0 @@ -685,7 +685,7 @@ __metadata: "@aws-sdk/util-utf8-node": 3.208.0 fast-xml-parser: 4.0.11 tslib: ^2.3.1 - checksum: 9a2ec3240bd01fd1ed95b2c6766ea8d141e4b85a6535fcbb5c57ac725ef58c2d1c1403a61c336adbb1eceb5dd306ac1d35d7b8f25ed1ebc544a14ad14fb038a7 + checksum: 98463fe50a588032b82be5289445c6e7250bd947076e5981ba7c126386d567f3b1ea9a9652c032da919503cdba2c3b517061911ddffae0c91f880f7eabaeaa08 languageName: node linkType: hard @@ -702,15 +702,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.235.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.236.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.235.0 + "@aws-sdk/client-cognito-identity": 3.236.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: 85d348faa5161282d226040315aaff1a754d3bbd23ccb9131f021a5f6a473be76b80e2b3f5132d3b0a119732822d6b76829626d64e6c71c4d61d1fcbeb6e0080 + checksum: 29d3178f490cbb711553103c0fb544604c5e70e36395f1edc6b9bd2ee0e928160b911b4893fe89418c182548d0dddfbd44e9ef629ad690ab1724a2a693368c16 languageName: node linkType: hard @@ -738,38 +738,38 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.235.0" +"@aws-sdk/credential-provider-ini@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.236.0" dependencies: "@aws-sdk/credential-provider-env": 3.226.0 "@aws-sdk/credential-provider-imds": 3.226.0 "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.235.0 + "@aws-sdk/credential-provider-sso": 3.236.0 "@aws-sdk/credential-provider-web-identity": 3.226.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/shared-ini-file-loader": 3.226.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: c910746d90ec363691dc99d53594beb49ea8562ef69c5a2e9a1702febc7d870cfe5b0f441afdae2bed643a4e8901b82f965660cdd70077bb006978fdafddeb05 + checksum: e748aba37dab11ed395fa60c7eee119b997e4c64b1a838c93258cfe6a6e289dfa0aa20d292b94d06da8148b7cb32604b24f84aadc2a389bf8248c64d80ef44d5 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.235.0, @aws-sdk/credential-provider-node@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.235.0" +"@aws-sdk/credential-provider-node@npm:3.236.0, @aws-sdk/credential-provider-node@npm:^3.208.0": + version: 3.236.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.236.0" dependencies: "@aws-sdk/credential-provider-env": 3.226.0 "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/credential-provider-ini": 3.235.0 + "@aws-sdk/credential-provider-ini": 3.236.0 "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.235.0 + "@aws-sdk/credential-provider-sso": 3.236.0 "@aws-sdk/credential-provider-web-identity": 3.226.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/shared-ini-file-loader": 3.226.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: bfea379a5e66c311c57a1f7390b2e508d42f8cf3e1a48a5b9585b33c76339c7e8806dba413c7d5bf6d669ffd05a4c55ef705f92bf4687ba8f5e359c5d3c7495e + checksum: d5a26b40a985edfef5006efde921d58b78829b67eed9fea56e108b99eec437a02721b93ef0e2dcd9a4739716c85d8db75935a71d2afa1190b0f1d0010511f649 languageName: node linkType: hard @@ -785,17 +785,17 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.235.0" +"@aws-sdk/credential-provider-sso@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.236.0" dependencies: - "@aws-sdk/client-sso": 3.235.0 + "@aws-sdk/client-sso": 3.236.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/shared-ini-file-loader": 3.226.0 - "@aws-sdk/token-providers": 3.235.0 + "@aws-sdk/token-providers": 3.236.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: 358fdda689c1f9be768516ca39de17e7fa7832d3d89d8893dd58f5d3377908ee2580155c1ba984c5ed21a410cda681bd22c7c74876af06b3496641a98b62353f + checksum: 215e0cd08c4dca453a80ce695bf4dddb94ce7562100bfa64e2ba5406ab3aeb6cf92882bd714b6ee95d3f13c45f3286eb616d32e1ffc58728f1e21e7eb171b301 languageName: node linkType: hard @@ -811,25 +811,25 @@ __metadata: linkType: hard "@aws-sdk/credential-providers@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/credential-providers@npm:3.235.0" + version: 3.236.0 + resolution: "@aws-sdk/credential-providers@npm:3.236.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.235.0 - "@aws-sdk/client-sso": 3.235.0 - "@aws-sdk/client-sts": 3.235.0 - "@aws-sdk/credential-provider-cognito-identity": 3.235.0 + "@aws-sdk/client-cognito-identity": 3.236.0 + "@aws-sdk/client-sso": 3.236.0 + "@aws-sdk/client-sts": 3.236.0 + "@aws-sdk/credential-provider-cognito-identity": 3.236.0 "@aws-sdk/credential-provider-env": 3.226.0 "@aws-sdk/credential-provider-imds": 3.226.0 - "@aws-sdk/credential-provider-ini": 3.235.0 - "@aws-sdk/credential-provider-node": 3.235.0 + "@aws-sdk/credential-provider-ini": 3.236.0 + "@aws-sdk/credential-provider-node": 3.236.0 "@aws-sdk/credential-provider-process": 3.226.0 - "@aws-sdk/credential-provider-sso": 3.235.0 + "@aws-sdk/credential-provider-sso": 3.236.0 "@aws-sdk/credential-provider-web-identity": 3.226.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/shared-ini-file-loader": 3.226.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: 3c8436f78a4ddcf295b06d77ed3cb912a8ae56ee047526663f8316dde6b646095c8288480214c9e3f68cb98772511cb7fcec6f8c4100fba899c495a9429c3ba2 + checksum: 3105661946fa12322b18a2227f26a8b734072850882e771493b398e714b433c024873a4495a18d55a4213022b07c658cb7ed38859b5fa152fc2ce1e02b408ca1 languageName: node linkType: hard @@ -967,8 +967,8 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.208.0": - version: 3.235.0 - resolution: "@aws-sdk/lib-storage@npm:3.235.0" + version: 3.236.0 + resolution: "@aws-sdk/lib-storage@npm:3.236.0" dependencies: "@aws-sdk/middleware-endpoint": 3.226.0 "@aws-sdk/smithy-client": 3.234.0 @@ -979,7 +979,7 @@ __metadata: peerDependencies: "@aws-sdk/abort-controller": ^3.0.0 "@aws-sdk/client-s3": ^3.0.0 - checksum: a9a8993fdc8c05769a2f0c5c838e9b2ba72216f234e67c1a8bcebf8f812ab397d48c8b118959f4d9918ad8811fdf93b3b47812075b0995af8c11cb26c0f3c305 + checksum: 783b67c4605a4f7d4072ce38b2b2416a0a6771784f9ba5b1608be3777e7d21a18c820fff94218856607f984c453376e25e80393ef233365476850e354f8e15f8 languageName: node linkType: hard @@ -1355,16 +1355,16 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.235.0": - version: 3.235.0 - resolution: "@aws-sdk/token-providers@npm:3.235.0" +"@aws-sdk/token-providers@npm:3.236.0": + version: 3.236.0 + resolution: "@aws-sdk/token-providers@npm:3.236.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.235.0 + "@aws-sdk/client-sso-oidc": 3.236.0 "@aws-sdk/property-provider": 3.226.0 "@aws-sdk/shared-ini-file-loader": 3.226.0 "@aws-sdk/types": 3.226.0 tslib: ^2.3.1 - checksum: 5b9cd8cfd697f04a7c628329af1cc5003ec25fe08626148c55389311b53ecbaa47b83f2c6b832fa847f9db2ca5a3f63bcb8a1059b1b3b3590010acf3d4610896 + checksum: c5e6c88f22d35002f2d575d23ba5a530ee195b7220d7c0c58603bc2fafb6632a6755bc863cb7519eadda110d2f3193a144f34cc0236df285fcde40c980ba653a languageName: node linkType: hard @@ -8231,9 +8231,6 @@ __metadata: "@manypkg/get-packages": ^1.1.3 "@microsoft/api-documenter": ^7.17.11 "@microsoft/api-extractor": ^7.23.0 - "@microsoft/api-extractor-model": ^7.17.2 - "@microsoft/tsdoc": 0.14.1 - "@microsoft/tsdoc-config": 0.16.2 "@types/is-glob": ^4.0.2 "@types/mock-fs": ^4.13.0 chalk: ^4.0.0 @@ -8245,6 +8242,9 @@ __metadata: mock-fs: ^5.1.0 ts-node: ^10.0.0 peerDependencies: + "@microsoft/api-extractor-model": "*" + "@microsoft/tsdoc": "*" + "@microsoft/tsdoc-config": "*" "@rushstack/node-core-library": "*" prettier: ^2.8.1 typescript: "> 3.0.0" @@ -9019,20 +9019,20 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.3.3": - version: 1.3.3 - resolution: "@eslint/eslintrc@npm:1.3.3" +"@eslint/eslintrc@npm:^1.4.0": + version: 1.4.0 + resolution: "@eslint/eslintrc@npm:1.4.0" dependencies: ajv: ^6.12.4 debug: ^4.3.2 espree: ^9.4.0 - globals: ^13.15.0 + globals: ^13.19.0 ignore: ^5.2.0 import-fresh: ^3.2.1 js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: f03e9d6727efd3e0719da2051ea80c0c73d20e28c171121527dbb868cd34232ca9c1d0525a66e517a404afea26624b1e47895b6a92474678418c2f50c9566694 + checksum: 73e39c833deafde8d8706e6fa9b52b6d99927c094ead8e405ea4174e8197ec24aac9ba88ae38cc8ad32eaccf07b9c7fc5dc70761d1fba6da41a928691447305f languageName: node linkType: hard @@ -9853,14 +9853,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.6": - version: 0.11.7 - resolution: "@humanwhocodes/config-array@npm:0.11.7" +"@humanwhocodes/config-array@npm:^0.11.8": + version: 0.11.8 + resolution: "@humanwhocodes/config-array@npm:0.11.8" dependencies: "@humanwhocodes/object-schema": ^1.2.1 debug: ^4.1.1 minimatch: ^3.0.5 - checksum: cf506dc45d9488af7fbf108ea6ac2151ba1a25e6d2b94b9b4fc36d2c1e4099b89ff560296dbfa13947e44604d4ca4a90d97a4fb167370bf8dd01a6ca2b6d83ac + checksum: 0fd6b3c54f1674ce0a224df09b9c2f9846d20b9e54fabae1281ecfc04f2e6ad69bf19e1d6af6a28f88e8aa3990168b6cb9e1ef755868c3256a630605ec2cb1d3 languageName: node linkType: hard @@ -11235,7 +11235,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor-model@npm:7.17.2, @microsoft/api-extractor-model@npm:^7.17.2": +"@microsoft/api-extractor-model@npm:7.17.2": version: 7.17.2 resolution: "@microsoft/api-extractor-model@npm:7.17.2" dependencies: @@ -11275,7 +11275,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc-config@npm:0.16.2, @microsoft/tsdoc-config@npm:~0.16.1": +"@microsoft/tsdoc-config@npm:~0.16.1": version: 0.16.2 resolution: "@microsoft/tsdoc-config@npm:0.16.2" dependencies: @@ -12470,8 +12470,8 @@ __metadata: linkType: hard "@rjsf/utils@npm:^5.0.0-beta.14": - version: 5.0.0-beta.14 - resolution: "@rjsf/utils@npm:5.0.0-beta.14" + version: 5.0.0-beta.15 + resolution: "@rjsf/utils@npm:5.0.0-beta.15" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -12480,20 +12480,20 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: 66d29377c98f6e778ec0b167734a949528b0ca2bfb4776dea74fd70add1fb339e14ec878b9162a152e83804bc8094c3ebcd9ebf8b1b68a63c6ee2bf4548938c8 + checksum: e22b1cc13183d6a6a57b900bb80825eff1c4680edadb554adb6737c21237c7862b2a249e5b67c0c6971094b5d21224daf108ad1f400286be924bbc5c29784a00 languageName: node linkType: hard "@rjsf/validator-ajv6@npm:^5.0.0-beta.14": - version: 5.0.0-beta.14 - resolution: "@rjsf/validator-ajv6@npm:5.0.0-beta.14" + version: 5.0.0-beta.15 + resolution: "@rjsf/validator-ajv6@npm:5.0.0-beta.15" dependencies: ajv: ^6.7.0 lodash: ^4.17.15 lodash-es: ^4.17.15 peerDependencies: "@rjsf/utils": ^5.0.0-beta.1 - checksum: 57b5c6968475a66762f644f2832aad077f8dde0ca9e71c8b0b27c06174855b7e6330f44fa6078fc0f347b728a4a5fd62399378fad26ada3cde32da89bddf6f9f + checksum: ce6d2cd110eee21e37ae08e4081b0f0fd65ac22f8c94ceeaff9f9a9482072897c3f988de1bc770c25031932117e7f71dda0bd166c2bb953af420d0ce1fd255d0 languageName: node linkType: hard @@ -17866,14 +17866,14 @@ __metadata: linkType: hard "canvas@npm:^2.10.2": - version: 2.10.2 - resolution: "canvas@npm:2.10.2" + version: 2.11.0 + resolution: "canvas@npm:2.11.0" dependencies: "@mapbox/node-pre-gyp": ^1.0.0 nan: ^2.17.0 node-gyp: latest simple-get: ^3.0.3 - checksum: b2e3eb4c3635fa2f67857619621c3d314f935a9e51904536dadf4908ab580dff4f5bcbaafe6eb0255247fa027ca494d5cd97c33376a49a0f994997263fa9944b + checksum: a69a6e0c90014a1b02e52c4c38a627d1a01ffe9539047bec84105cb3554907a947cf39b4a333be43fc1583dd142b641bb5480a4e23f59c6098618c33bf78f67f languageName: node linkType: hard @@ -21670,11 +21670,11 @@ __metadata: linkType: hard "eslint@npm:^8.6.0": - version: 8.29.0 - resolution: "eslint@npm:8.29.0" + version: 8.30.0 + resolution: "eslint@npm:8.30.0" dependencies: - "@eslint/eslintrc": ^1.3.3 - "@humanwhocodes/config-array": ^0.11.6 + "@eslint/eslintrc": ^1.4.0 + "@humanwhocodes/config-array": ^0.11.8 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 ajv: ^6.10.0 @@ -21693,7 +21693,7 @@ __metadata: file-entry-cache: ^6.0.1 find-up: ^5.0.0 glob-parent: ^6.0.2 - globals: ^13.15.0 + globals: ^13.19.0 grapheme-splitter: ^1.0.4 ignore: ^5.2.0 import-fresh: ^3.0.0 @@ -21714,7 +21714,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: e05204b05907b82d910983995cb946e0ba62ca514eb2b6791c43f623333b143564a2eee0139909d31c10935c21877d815b1f76dd674a59cb91c471064325c4ab + checksum: b7525bb465b342665c3b8bab7e114d514ef1bc4e79f211c919863f9c71767e7412ec82383a22614a92d159783f91101018817000f7c61ce69a5e7015280cafaf languageName: node linkType: hard @@ -23516,12 +23516,12 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.15.0": - version: 13.15.0 - resolution: "globals@npm:13.15.0" +"globals@npm:^13.19.0": + version: 13.19.0 + resolution: "globals@npm:13.19.0" dependencies: type-fest: ^0.20.2 - checksum: 383ade0873b2ab29ce6d143466c203ed960491575bc97406395e5c8434026fb02472ab2dfff5bc16689b8460269b18fda1047975295cd0183904385c51258bae + checksum: a000dbd00bcf28f0941d8a29c3522b1c3b8e4bfe4e60e262c477a550c3cbbe8dbe2925a6905f037acd40f9a93c039242e1f7079c76b0fd184bc41dcc3b5c8e2e languageName: node linkType: hard