diff --git a/.changeset/cool-toys-flow.md b/.changeset/cool-toys-flow.md new file mode 100644 index 0000000000..799bf3ace4 --- /dev/null +++ b/.changeset/cool-toys-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Add `transformLinkUri` and `transformImageUri` to `MarkdownContent` diff --git a/.changeset/eight-suits-fail.md b/.changeset/eight-suits-fail.md new file mode 100644 index 0000000000..742154da91 --- /dev/null +++ b/.changeset/eight-suits-fail.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Fixed the lack of `limitranges` as part of the Default Objects to fetch from the kubernetes api diff --git a/.changeset/five-fireants-run.md b/.changeset/five-fireants-run.md new file mode 100644 index 0000000000..589d46cc81 --- /dev/null +++ b/.changeset/five-fireants-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix the EntityLayout header style so that EntityContextMenu button can display in correct shape when user hover on it diff --git a/.changeset/hot-rice-sin.md b/.changeset/hot-rice-sin.md new file mode 100644 index 0000000000..5a3d6fdb7a --- /dev/null +++ b/.changeset/hot-rice-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Add `allowedRepos` `ui:option` to `RepoUrlPicker` component, and move `repoName` field to own component diff --git a/.changeset/moody-crabs-march.md b/.changeset/moody-crabs-march.md new file mode 100644 index 0000000000..b5ddf12ffa --- /dev/null +++ b/.changeset/moody-crabs-march.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-vault': patch +'@backstage/plugin-vault-backend': patch +--- + +Added a path notion in addition to secret name to allow to differentiate secrets in sub-paths diff --git a/.changeset/old-onions-hear.md b/.changeset/old-onions-hear.md new file mode 100644 index 0000000000..82dcc09190 --- /dev/null +++ b/.changeset/old-onions-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Update GitLab Merge Request Action to allow source branch to be deleted diff --git a/.changeset/proud-toys-return.md b/.changeset/proud-toys-return.md new file mode 100644 index 0000000000..f93e6ef4a8 --- /dev/null +++ b/.changeset/proud-toys-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Introduced `GroupDefaultParentEntityPolicy` to set a default group entity parent. diff --git a/.changeset/renovate-149779d.md b/.changeset/renovate-149779d.md new file mode 100644 index 0000000000..58d764485e --- /dev/null +++ b/.changeset/renovate-149779d.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka-backend': patch +--- + +Updated dependency `kafkajs` to `^2.0.0`. diff --git a/.changeset/renovate-7438bff.md b/.changeset/renovate-7438bff.md new file mode 100644 index 0000000000..43a4ac5016 --- /dev/null +++ b/.changeset/renovate-7438bff.md @@ -0,0 +1,16 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-tasks': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Updated dependency `knex` to `^2.0.0`. diff --git a/.changeset/techdocs-sixty-mugs-hug.md b/.changeset/techdocs-sixty-mugs-hug.md new file mode 100644 index 0000000000..a1a9e815a1 --- /dev/null +++ b/.changeset/techdocs-sixty-mugs-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fix: When docs are shown in an entity page under the docs tab the sidebars start overlapping with the header and tabs in the page when you scroll the documentation content. diff --git a/.changeset/thirty-rivers-watch.md b/.changeset/thirty-rivers-watch.md new file mode 100644 index 0000000000..d92343f365 --- /dev/null +++ b/.changeset/thirty-rivers-watch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added two new scaffolder actions: `github:repo:create` and `github:repo:push` diff --git a/.changeset/weak-jeans-cry.md b/.changeset/weak-jeans-cry.md new file mode 100644 index 0000000000..79f419e3db --- /dev/null +++ b/.changeset/weak-jeans-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add `allowArbitraryValues` to `ui:options` in `OwnedEntityPicker`, similar to `allowArbitraryValues` in `EntityPicker` diff --git a/.changeset/wicked-ladybugs-argue.md b/.changeset/wicked-ladybugs-argue.md new file mode 100644 index 0000000000..9a36cf17cb --- /dev/null +++ b/.changeset/wicked-ladybugs-argue.md @@ -0,0 +1,43 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Added new `EntityProcessingStatusPicker` that will filter for entities with orphans and/or errors. + +If you are using the default Catalog page this picker will be added automatically. For those who have customized their Catalog page you'll need to add this manually by doing something like this: + +```diff +... +import { + CatalogFilterLayout, + EntityTypePicker, + UserListPicker, + EntityTagPicker ++ EntityProcessingStatusPicker, +} from '@backstage/plugin-catalog-react'; +... +export const CustomCatalogPage = ({ + columns, + actions, + initiallySelectedFilter = 'owned', +}: CatalogPageProps) => { + return ( + ... + + + + + + ... +}; +``` diff --git a/.github/workflows/automate_merge_renovate_prs.yaml b/.github/workflows/automate_merge_renovate_prs.yaml deleted file mode 100644 index 8de488cf05..0000000000 --- a/.github/workflows/automate_merge_renovate_prs.yaml +++ /dev/null @@ -1,85 +0,0 @@ -name: Automate Merge Renovate PRs - -on: - workflow_dispatch: - schedule: - - cron: '*/10 * * * *' - -jobs: - diff: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - with: - script: | - const owner = "backstage"; - const repo = "backstage"; - const query = `{ - repository(owner: "backstage", name: "backstage") { - pullRequests(labels: ["dependencies"], last: 10, states: [OPEN]) { - nodes { - title - author { - login - } - number - mergeable - files(first: 1) { - nodes { - path - } - } - changedFiles - commits(last: 1) { - nodes { - commit { - statusCheckRollup { - state - } - } - } - } - reviewDecision - reviews(first: 10) { - nodes { - author { - login - } - } - } - } - } - } - }`; - - const date = new Date(); - if (date.getDay() === 2) { - console.log("Skipping auto merge because Tuesday is release day"); - return; - } - - const r = await github.graphql(query); - const mergable = r.repository.pullRequests.nodes.filter( - (pr) => - pr.author.login === "renovate" && - pr.mergeable === "MERGEABLE" && - pr.changedFiles === 1 && - pr.files.nodes[0].path.split("/").slice(-1)[0] === "yarn.lock" && - pr.commits.nodes[0].commit.statusCheckRollup.state === "SUCCESS" && - pr.reviewDecision === "APPROVED" - ); - - if (mergable.length === 0) { - console.log("no mergable PRs"); - return; - } - - for (const pr of mergable) { - console.log(`Merging #${pr.number} - ${pr.title}`); - await github.rest.pulls.merge({ - owner, - repo, - pull_number: pr.number, - }); - await new Promise((r) => setTimeout(r, 2000)); - } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0718c8a8b9..c9fc151fb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,45 +64,16 @@ jobs: - name: fetch branch master run: git fetch origin master - # Beginning of yarn setup, keep in sync between all workflows. - # TODO(Rugvip): move this to composite action once all features we use are supported - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - # Cache every node_modules folder inside the monorepo - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - # We use both yarn.lock and package.json as cache keys to ensure that - # changes to local monorepo packages bust the cache. - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - # If we get a cache hit for node_modules, there's no need to bring in the global - # yarn cache or run yarn install, as all dependencies will be installed already. - - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: yarn install - if: steps.cache-modules.outputs.cache-hit != 'true' - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: check for yarn.lock changes id: yarn-lock diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 0000000000..ca21c1c283 --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,15 @@ +name: Cron +on: + workflow_dispatch: + schedule: + - cron: '*/5 * * * *' + +jobs: + cron: + runs-on: ubuntu-latest + steps: + - uses: backstage/actions/cron@v0.2.2 + with: + app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} + private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} + installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index c90894bce4..97b3f45954 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -20,33 +20,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} # No verification done here, only build & publish. If the master branch # is broken we will see that from those builds, but we still want to push nightly diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index b63507df0c..529c7a4286 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -61,33 +61,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: Fetch previous commit for release check run: git fetch origin '${{ github.event.before }}' @@ -157,33 +139,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: build type declarations run: yarn tsc:full diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml new file mode 100644 index 0000000000..5fa11b839f --- /dev/null +++ b/.github/workflows/issue.yaml @@ -0,0 +1,13 @@ +name: Issue +on: + issues: + types: [opened] + +jobs: + sync: + runs-on: ubuntu-latest + + if: github.repository == 'backstage/backstage' + steps: + - name: Issue sync + uses: backstage/actions/issue-sync@v0.2.1 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000000..e65240ac1b --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,14 @@ +name: PR +on: + pull_request_target: + +jobs: + sync: + runs-on: ubuntu-latest + + if: github.repository == 'backstage/backstage' + steps: + - name: PR sync + uses: backstage/actions/pr-sync@v0.2.1 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} diff --git a/.github/workflows/sync_approve_renovate_pr.yaml b/.github/workflows/sync_approve_renovate_pr.yaml deleted file mode 100644 index 2d25bea223..0000000000 --- a/.github/workflows/sync_approve_renovate_pr.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Approve renovate lock file changes -on: - pull_request_target: - paths: - - '.github/workflows/sync_renovate-changesets.yml' - - '**/yarn.lock' - -jobs: - generate-changeset: - runs-on: ubuntu-latest - if: github.actor == 'renovate[bot]' && github.repository == 'backstage/backstage' - steps: - - name: Approve - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - script: | - const owner = 'backstage'; - const repo = 'backstage'; - - const r = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: context.issue.number, - }); - - if (r.data.some((f) => f.filename.split('/').slice(-1)[0] !== 'yarn.lock')) { - console.log('skipping approval since some files are not yarn.lock'); - return; - } - - await github.rest.pulls.createReview({ - owner, - repo, - pull_number: context.issue.number, - event: 'APPROVE' - }) diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index b6fce0e68d..6ea6bc5de6 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -14,33 +14,15 @@ jobs: # Fetch changes to previous commit - required for 'only_changed' in Prettier action fetch-depth: 0 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: Run Prettier on ADOPTERS.md uses: creyD/prettier_action@v4.2 diff --git a/.github/workflows/sync_issue-labels.yml b/.github/workflows/sync_issue-labels.yml deleted file mode 100644 index 91b5f79f5d..0000000000 --- a/.github/workflows/sync_issue-labels.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Sync Issue Labels -on: - issues: - types: [opened] -jobs: - label-issue: - runs-on: ubuntu-latest - if: github.repository == 'backstage/backstage' - steps: - - name: View context attributes - uses: actions/github-script@v6 - with: - script: | - const keywords = { - 'techdocs|tech-docs|tech docs': 'docs-like-code', - 'search': 'search', - 'catalog': 'catalog', - 'scaffolder': 'scaffolder', - }; - - const labels = Object.entries(keywords) - .map(([regexp, label]) => { - if (new RegExp(regexp, 'gi').test(context.payload.issue.title)) { - return label; - } - }) - .filter(Boolean); - - if(!labels.length) { - return; - } - - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels - }); diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index e45a36c8e8..9daf85ffa6 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -16,33 +16,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: Create Snyk report uses: snyk/actions/node@master diff --git a/.github/workflows/verify_dco.yaml b/.github/workflows/verify_dco.yaml deleted file mode 100644 index 2094327d23..0000000000 --- a/.github/workflows/verify_dco.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Verify DCO -on: - schedule: - - cron: '*/15 * * * *' - -jobs: - dco-helper: - runs-on: ubuntu-latest - steps: - - name: Verify DCO status for open pull requests - uses: actions/github-script@v6 - with: - script: | - const owner = "backstage"; - const repo = "backstage"; - const pulls = await github.paginate(github.rest.pulls.list, { - state: "open", - owner, - repo, - }); - - for (const pull of pulls) { - // Pick out the PRs that have the DCO check - const checks = await github.rest.checks.listForRef({ - owner, - repo, - ref: pull.head.sha, - check_name: "DCO", - status: "completed", - }); - // Skip if there are no checks - if (!checks.data.check_runs.length) { - continue; - } - // Skip if the conclusion is not action_required - if (checks.data.check_runs[0].conclusion !== "action_required") { - console.log(`No checks found for PR #${pull.number}, skipping`); - continue; - } - - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: pull.number, - }); - if (comments.find((c) => - c.user.login === "github-actions[bot]" && - c.body.includes("") - ) - ) { - console.log(`already commented on PR #${pull.number}, skipping`); - continue; - } - console.log(`creating comment on PR #${pull.number}`); - const body = ` - Thanks for the contribution! - All commits need to be DCO signed before they are reviewed. Please refer to the the [DCO section in CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#developer-certificate-of-origin) or the [DCO](${checks.data.check_runs[0].html_url}) status for more info. - `; - await github.rest.issues.createComment({ - repo, - owner, - issue_number: pull.number, - body, - }); - } diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 884d8bf172..0fe18b345e 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -40,35 +40,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: setup chrome - uses: browser-actions/setup-chrome@latest - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - name: yarn install - run: yarn install --frozen-lockfile - # End of yarn setup + uses: backstage/actions/yarn-install@v0.2.1 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - run: yarn tsc - run: yarn backstage-cli repo build diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index a13aa3ae71..7bbbac8a40 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -17,44 +17,30 @@ on: jobs: chromatic: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + node-version: [16.x] + steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # Required to retrieve git history - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: cache all node_modules - id: cache-modules - uses: actions/cache@v3 + - name: yarn install + uses: backstage/actions/yarn-install@v0.2.1 with: - path: '**/node_modules' - key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} - - name: find location of global yarn cache - id: yarn-cache - if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - if: steps.cache-modules.outputs.cache-hit != 'true' - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: top-level install - run: yarn install --frozen-lockfile - + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install run: yarn install --frozen-lockfile working-directory: storybook - # End of yarn setup - run: yarn build-storybook diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index bb8aa9e5aa..e286d42c7e 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -26,25 +26,15 @@ jobs: steps: - uses: actions/checkout@v3 - # Beginning of yarn setup, keep in sync between all workflows, see ci.yml - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - - name: find location of global yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: cache global yarn cache - uses: actions/cache@v3 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- + + # Windows file operation slowness means there's no point caching this - name: yarn install run: yarn install --frozen-lockfile - # End of yarn setup - name: lint run: yarn backstage-cli repo lint diff --git a/ADOPTERS.md b/ADOPTERS.md index b72da367b1..2f47f22f90 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,6 +1,8 @@ # Adopters -_If you're using Backstage in your organization, please try to add your company name to this list. This really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ +_If you're using Backstage in your organization, please try to add your company name to this list. It really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ +_You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKikB) or by editing this file after following the [CONTRIBUTING.md](./CONTRIBUTING.md)._ + | Organization | Contact | Description of Use | |-----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -36,7 +38,7 @@ _If you're using Backstage in your organization, please try to add your company | [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | | [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | | [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [DAZN](https://dazn.com/) | [David Rubio Vidal](https://github.com/davidrv87), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | | [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | | [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | | [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | @@ -186,3 +188,5 @@ _If you're using Backstage in your organization, please try to add your company | [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. | | [William Hill](https://www.williamhillgroup.com/) | [Pat Mills](mailto:pat.mills@williamhill.com), [Nathan Flynn](mailto:nflynn@williamhill.co.uk), and [Nishkarsh Raj](mailto:nishkarsh.raj@williamhill.co.uk) | William Hill are leveraging Backstage to build our Engineering Portal. Our mission is to centralize the software catalog inventory to enable service discoverability, reduce the onboarding time for new Engineers, provide a single pane of glass to accelerate Developer Productivity and Save Engineers time. Our aspiration is to create an InnerSource community focussed on organization-wide patterns that are re-usable and can be self-served with the Scaffolder. | | [Vodafone NewZealand Limited](https://vodafone.co.nz) | [Ankit Gupta](mailto:ankit.gupta@vodafone.nz), [DevOps COE](mailto:devopstooling@vodafone.nz) | Vodafone NZ are leveraging Backstage to build centralised and self service Engineering Portal. Our mission is to standardised Pipeline templates across the Engineering teams, One shop stop to create the pipelines and repository with a template approach which reduces creation part from days to minutes and no wait time for developers. A unified view for Azure DevOps pipeline, Azure Repo pull requests, Deployment status from Azure RedHat Openshift-ArgoCD and SonarQube Security and code quality scans report on a single pan to provide a streamlined view for all microservices across the app stack. | +| [Coamo](http://www.coamo.com.br) | [@holiiveira](https://github.com/holiiveira), [@gpxlnx](https://github.com/gpxlnx) | We're starting to use it as the main tool of a DevOps platform. Our goal is to provide software templates, centralize our software catalog enabling efficient service discovery, and make it easy to manage the entire software ecosystem in one place. + | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb60bc48e0..1afbf072fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,9 +58,9 @@ If you are proposing a feature: - Remember that this is a volunteer-driven project, and that contributions are welcome :) -### Add your company to ADOPTERS +### Add your company to `ADOPTERS` -Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project. +Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project, you can do this by filling out this [Adopter form](https://form.typeform.com/to/zcOaKikB). ## Get Started! diff --git a/cypress/yarn.lock b/cypress/yarn.lock index 1ccce4ea24..991d2d8562 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -304,9 +304,9 @@ cross-spawn@^7.0.0: which "^2.0.1" cypress@^10.0.0: - version "10.2.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" - integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== + version "10.3.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae" + integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 849823a59e..5abd266099 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -106,8 +106,7 @@ with [Static Location Configuration](#static-location-configuration) or a discovery processor like [GitHub Discovery](../../integrations/github/discovery.md). To enforce usage of processors to locate entities we can configure the catalog into `readonly` mode. -This configuration disables the mutating backend catalog APIs and disallows -users from registering new entities at run-time. +This configuration disables registering and deleting locations with the catalog APIs. ```yaml catalog: @@ -117,6 +116,8 @@ catalog: > **Note that any plugin relying on the catalog API for creating, updating and > deleting entities will not work in this mode.** +Deleting an entity by UUID, `DELETE /entities/by-uid/:uid`, is allowed when using this mode. It may be rediscovered as noted in [explicit deletion](life-of-an-entity.md#explicit-deletion). + A common use case for this configuration is when organizations have a remote source that should be mirrored into Backstage. To make Backstage a mirror of this remote source, users cannot also register new entities with e.g. the diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index db765fc7ee..e8176a688b 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -86,6 +86,13 @@ contains more information about the required fields. Once we have a `template.yaml` ready, we can then add it to the software catalog for use by the scaffolder. +> Note: When you add or modify a template, you will need to refresh the location entity. +> Otherwise, Backstage won't display the template in the available templates, +> or it will keep showing the old template. You can refresh the location instance by +> going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct location entity. +> From there, you can click on the refresh icon representing "Scheduled entity refresh" action. +> Afterwards, you should see your template updated. + You can add the template files to the catalog through [static location configuration](../software-catalog/configuration.md#static-location-configuration), for example: @@ -97,6 +104,8 @@ catalog: target: https://github.com/backstage/software-templates/blob/main/scaffolder-templates/react-ssr-template/template.yaml rules: - allow: [Template] + - type: file + target: template.yaml # Backstage will expect the file to be in packages/backend/template.yaml ``` Or you can add the template using the `catalog-import` plugin, which unless diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 8e698e1a05..a9e1d297f3 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -9,8 +9,9 @@ by writing custom actions which can be used along side our [built-in actions](./builtin-actions.md). > Note: When adding custom actions, the actions array will **replace the -> built-in actions too**. To ensure you can continue to include the builtin -> actions, see below to include them during registration of your action. +> built-in actions too**. Meaning, you will no longer be able to use them. +> If you want to continue using the builtin actions, include them in the actions +> array when registering your custom actions, as seen below. ## Writing your Custom Action @@ -122,27 +123,32 @@ will set the available actions that the scaffolder has access to. ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; import { ScmIntegrations } from '@backstage/integration'; +import { createNewFileAction } from './actions/custom'; -const integrations = ScmIntegrations.fromConfig(env.config); +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); + const integrations = ScmIntegrations.fromConfig(env.config); -const builtInActions = createBuiltinActions({ - containerRunner, - integrations, - catalogClient, - config: env.config, - reader: env.reader, -}); + const builtInActions = createBuiltinActions({ + integrations, + catalogClient, + config: env.config, + reader: env.reader, + }); -const actions = [...builtInActions, createNewFileAction()]; -return await createRouter({ - containerRunner, - catalogClient, - actions, - logger: env.logger, - config: env.config, - database: env.database, - reader: env.reader, -}); + const actions = [...builtInActions, createNewFileAction()]; + + return createRouter({ + actions, + catalogClient: catalogClient, + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, + }); +} ``` ## List of custom action packages diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index ff9962d8d4..81367b19c5 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -320,6 +320,30 @@ The `allowedHosts` part should be set to where you wish to enable this template to publish to. And it can be any host that is listed in your `integrations` config in `app-config.yaml`. +Besides specifying `allowedHosts` you can also restrict the template to publish to +repositories owned by specific users/groups/namespaces by setting the `allowedOwners` +option. With the `allowedRepos` option you are able to narrow it down further to a +specific set of repository names. A full example could look like this: + +```yaml +- title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + allowedOwners: + - backstage + - someGithubUser + allowedRepos: + - backstage +``` + The `RepoUrlPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) @@ -485,6 +509,64 @@ the value of `firstName` from the parameters). This is great for passing the values from the form into different steps and reusing these input variables. These template strings preserve the type of the parameter. +The `${{ parameters.firstName }}` pattern will work only in the template file. +If you want to start using values provided from the UI in your code, you will have to use +the `${{ values.firstName }}` pattern. Additionally, you have to pass +the parameters from the UI to the input of the `fetch:template` step. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Test Action + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of your project + urlParameter: + title: URL endpoint + type: string + description: URL endpoint at which the component can be reached + default: 'https://www.example.com' + enabledDB: + title: Enable Database + type: boolean + default: false + ... + steps: + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./template + values: + name: ${{ parameters.name }} + url: ${{ parameters.urlParameter }} + enabledDB: ${{ parameters.enabledDB }} +``` + +Afterwards, if you are using the builtin templating action, you can start using +the variables in your code. You can use also any other templating functions from +[Nunjucks](https://mozilla.github.io/nunjucks/templating.html#tags) as well. + +```bash +#!/bin/bash +echo "Hi my name is ${{ values.name }}, and you can fine me at ${{ values.url }}!" +{% if values.enabledDB %} +echo "You have enabled your database!" +{% endif %} +``` + As you can see above in the `Outputs` section, `actions` and `steps` can also output things. You can grab that output using `steps.$stepId.output.$property`. diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 831729d093..fe1058b2ca 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -52,6 +52,8 @@ Addons are rendered in the order in which they are registered. ## Installing and using Addons +To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn add --cwd packages/app @backstage/plugin-techdocs-module-addons-contrib` + Addons can be installed and configured in much the same way as extensions for other Backstage plugins: by adding them underneath an extension registry component (``) under the route representing the TechDocs Reader @@ -74,6 +76,18 @@ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; ; ``` +If you are using a custom [TechDocs reader page](./how-to-guides.md#how-to-customize-the-techdocs-reader-page) your setup will be very similar, here's an example: + +```ts +}> + + + {/* Other addons can be added here. */} + + {techDocsPage} // This is your custom TechDocs reader page + +``` + The process for configuring Addons on the documentation tab on the entity page is very similar; instead of adding the `` registry under a ``, you'd add it as a child of ``: diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 9440d9a824..8a54d76ed0 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -160,7 +160,8 @@ techdocs: # techdocs.cache is optional, and is only recommended when you've configured # an external techdocs.publisher.type above. Also requires backend.cache to - # be configured with a valid cache store. + # be configured with a valid cache store. Configure techdocs.cache.ttl to + # enable caching of techdocs assets. cache: # Represents the number of milliseconds a statically built asset should # stay cached. Cache invalidation is handled automatically by the frontend, diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index ca88a9d190..8e1df125fc 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -65,7 +65,7 @@ "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", "@keyv/redis": "^2.2.3", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "logform": "^2.3.2", "luxon": "^2.3.1", diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 503945e594..004e7d13cd 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -251,7 +251,7 @@ export class DatabaseManager { // include base connection if client type has not been overridden ...(overridden ? {} : baseConnection), ...connection, - }; + } as Partial; } /** diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 8d69f2ce7c..80655db491 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -39,7 +39,7 @@ "@backstage/types": "^1.0.0", "@types/luxon": "^2.0.4", "cron": "^2.0.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-abort-controller": "^3.0.1", diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 7335447797..ff1275c387 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -38,7 +38,7 @@ "@backstage/cli": "^0.18.0-next.1", "@backstage/config": "^1.0.1", "better-sqlite3": "^7.5.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "msw": "^0.42.0", "mysql2": "^2.2.5", "pg": "^8.3.0", diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 3eeb054174..9b9136fc3c 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -230,6 +230,13 @@ export function getEntitySourceLocation(entity: Entity): { target: string; }; +// @public +export class GroupDefaultParentEntityPolicy implements EntityPolicy { + constructor(parentEntityRef: string); + // (undocumented) + enforce(entity: Entity): Promise; +} + // @public interface GroupEntityV1alpha1 extends Entity { // (undocumented) diff --git a/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.test.ts new file mode 100644 index 0000000000..d5eb6145c2 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UserEntity, GroupEntity } from '../../kinds'; +import { GroupDefaultParentEntityPolicy } from './GroupDefaultParentEntityPolicy'; + +describe('GroupDefaultParentEntityPolicy', () => { + it('should ignore non-group entities', async () => { + const p = new GroupDefaultParentEntityPolicy('name'); + const u: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n' }, + spec: { profile: {}, memberOf: ['c'] }, + }; + const result = await p.enforce(u); + expect(result).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n' }, + spec: { profile: {}, memberOf: ['c'] }, + }); + }); + + it('should parent group entities', async () => { + const p = new GroupDefaultParentEntityPolicy('name'); + const g: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'n' }, + spec: { type: 'foo', children: [] }, + }; + const result = await p.enforce(g); + expect(result).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'n' }, + spec: { type: 'foo', parent: 'group:default/name', children: [] }, + }); + }); + + it('should not replace existing parents', async () => { + const p = new GroupDefaultParentEntityPolicy('namespace/name'); + const g: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'n' }, + spec: { type: 'foo', parent: 'group:something/else', children: [] }, + }; + const result = await p.enforce(g); + expect(result).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'n' }, + spec: { type: 'foo', parent: 'group:something/else', children: [] }, + }); + }); +}); diff --git a/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.ts b/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.ts new file mode 100644 index 0000000000..8eb0aef757 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/GroupDefaultParentEntityPolicy.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '../Entity'; +import { GroupEntity } from '../../kinds'; +import { EntityPolicy } from './types'; +import { DEFAULT_NAMESPACE } from '../constants'; +import { parseEntityRef, stringifyEntityRef } from '../ref'; + +/** + * DefaultParentPolicy is an EntityPolicy that updates group entities + * with a parent of last resort. This ensures that, while we preserve + * any existing group hierarchies, we can guarantee that there is a + * single global root of the group hierarchy. + * + * @public + */ +export class GroupDefaultParentEntityPolicy implements EntityPolicy { + private readonly parentRef: string; + + constructor(parentEntityRef: string) { + const { kind, namespace, name } = parseEntityRef(parentEntityRef, { + defaultKind: 'Group', + defaultNamespace: DEFAULT_NAMESPACE, + }); + + if (kind.toLocaleUpperCase('en-US') !== 'GROUP') { + throw new TypeError('group parent must be a group'); + } + + this.parentRef = stringifyEntityRef({ + kind: kind, + namespace: namespace, + name: name, + }); + } + + async enforce(entity: Entity): Promise { + if (entity.kind !== 'Group') { + return entity; + } + + const group = entity as GroupEntity; + if (group.spec.parent) { + return group; + } + + // Avoid making the parent entity it's own parent. + if (stringifyEntityRef(group) !== this.parentRef) { + group.spec.parent = this.parentRef; + } + + return group; + } +} diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts index ae14007d80..d5740c0955 100644 --- a/packages/catalog-model/src/entity/policies/index.ts +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -15,6 +15,7 @@ */ export { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy'; +export { GroupDefaultParentEntityPolicy } from './GroupDefaultParentEntityPolicy'; export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx index 2e1550078f..9c08b83c40 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.test.tsx @@ -62,4 +62,42 @@ describe('', () => { expect(fp2).toBeInTheDocument(); expect(rendered.getByText(');', { selector: 'span' })).toBeInTheDocument(); }); + + it('render MarkdownContent component with transformed link', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + { + return `${href}-modified`; + }} + />, + ), + ); + const fp1 = rendered.getByText('Title', { + selector: 'a', + }); + expect(fp1).toBeInTheDocument(); + expect(fp1.getAttribute('href')).toEqual( + 'https://backstage.io/link-modified', + ); + }); + + it('render MarkdownContent component with transformed image', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + { + return `https://example.com/blog/assets/6/header.png`; + }} + />, + ), + ); + const fp1 = rendered.getByAltText('Image'); + expect(fp1).toBeInTheDocument(); + expect(fp1.getAttribute('src')).toEqual( + 'https://example.com/blog/assets/6/header.png', + ); + }); }); diff --git a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx index df4ad56776..0e5aedda90 100644 --- a/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx +++ b/packages/core-components/src/components/MarkdownContent/MarkdownContent.tsx @@ -68,6 +68,8 @@ type Props = { content: string; dialect?: 'gfm' | 'common-mark'; linkTarget?: Options['linkTarget']; + transformLinkUri?: (href: string) => string; + transformImageUri?: (href: string) => string; }; const components: Options['components'] = { @@ -91,7 +93,13 @@ const components: Options['components'] = { * If you just want to render to plain [CommonMark](https://commonmark.org/), set the dialect to `'common-mark'` */ export function MarkdownContent(props: Props) { - const { content, dialect = 'gfm', linkTarget } = props; + const { + content, + dialect = 'gfm', + linkTarget, + transformLinkUri, + transformImageUri, + } = props; const classes = useStyles(); return ( ); } diff --git a/packages/core-components/src/layout/Header/Header.tsx b/packages/core-components/src/layout/Header/Header.tsx index bb4d19b949..a32d116b56 100644 --- a/packages/core-components/src/layout/Header/Header.tsx +++ b/packages/core-components/src/layout/Header/Header.tsx @@ -63,6 +63,7 @@ const useStyles = makeStyles( }, rightItemsBox: { width: 'auto', + alignItems: 'center', }, title: { color: theme.palette.bursts.fontColor, diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 9277e60485..b76c4c00dd 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -43,7 +43,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", "helmet": "^5.0.2", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "winston": "^3.2.1", diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 3b7ac26295..624899c38f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -53,7 +53,7 @@ "google-auth-library": "^8.0.0", "jose": "^4.6.0", "jwt-decode": "^3.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "minimatch": "^5.0.0", diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index a329db4f9a..08e39852f5 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -29,7 +29,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 5cef11c8d4..fa2d84e57e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -55,7 +55,7 @@ "fs-extra": "10.1.0", "git-url-parse": "^11.6.0", "glob": "^7.1.6", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-fetch": "^2.6.7", diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 285ecafd70..5232c49150 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -78,6 +78,7 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntitySearchBar: CatalogReactEntitySearchBarClassKey; CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; + CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; }; // @public (undocumented) @@ -86,6 +87,9 @@ export type CatalogReactEntityLifecyclePickerClassKey = 'input'; // @public (undocumented) export type CatalogReactEntityOwnerPickerClassKey = 'input'; +// @public (undocumented) +export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; + // @public (undocumented) export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; @@ -137,8 +141,19 @@ export type DefaultEntityFilters = { lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; text?: EntityTextFilter; + orphan?: EntityOrphanFilter; + error?: EntityErrorFilter; }; +// @public +export class EntityErrorFilter implements EntityFilter { + constructor(value: boolean); + // (undocumented) + filterEntity(entity: Entity): boolean; + // (undocumented) + readonly value: boolean; +} + // @public (undocumented) export type EntityFilter = { getCatalogFilters?: () => Record< @@ -222,6 +237,15 @@ export type EntityLoadingStatus = { refresh?: VoidFunction; }; +// @public +export class EntityOrphanFilter implements EntityFilter { + constructor(value: boolean); + // (undocumented) + filterEntity(entity: Entity): boolean; + // (undocumented) + readonly value: boolean; +} + // @public export class EntityOwnerFilter implements EntityFilter { constructor(values: string[]); @@ -236,6 +260,9 @@ export class EntityOwnerFilter implements EntityFilter { // @public (undocumented) export const EntityOwnerPicker: () => JSX.Element | null; +// @public (undocumented) +export const EntityProcessingStatusPicker: () => JSX.Element; + // @public export const EntityProvider: (props: EntityProviderProps) => JSX.Element; diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx new file mode 100644 index 0000000000..8e90e04148 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx @@ -0,0 +1,152 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { EntityErrorFilter, EntityOrphanFilter } from '../../filters'; +import { MockEntityListContextProvider } from '../../testUtils/providers'; +import { EntityProcessingStatusPicker } from './EntityProcessingStatusPicker'; + +const orphanAnnotation: Record = {}; +orphanAnnotation['backstage.io/orphan'] = 'true'; + +const sampleEntities: Entity[] = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'valid-component', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'orphan-component', + annotations: orphanAnnotation, + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'error-component', + tags: ['Invalid Tag'], + }, + }, +]; + +describe('', () => { + it('renders all processing status options', () => { + const rendered = render( + + + , + ); + expect(rendered.getByText('Processing Status')).toBeInTheDocument(); + + fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); + expect(rendered.getByText('Is Orphan')).toBeInTheDocument(); + expect(rendered.getByText('Has Error')).toBeInTheDocument(); + }); + + it('adds orphan to orphan filter', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + + fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); + fireEvent.click(rendered.getByText('Is Orphan')); + expect(updateFilters).toHaveBeenCalledWith({ + orphan: new EntityOrphanFilter(true), + }); + }); + + it('adds error to error filter', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + + fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); + fireEvent.click(rendered.getByText('Has Error')); + expect(updateFilters).toHaveBeenCalledWith({ + error: new EntityErrorFilter(true), + }); + }); + + it('remove orphan from orphan filter', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + + fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); + fireEvent.click(rendered.getByText('Is Orphan')); + expect(updateFilters).toHaveBeenCalledWith({ + orphan: undefined, + }); + }); + + it('remove error from error filter', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + + fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); + fireEvent.click(rendered.getByText('Has Error')); + expect(updateFilters).toHaveBeenCalledWith({ + error: undefined, + }); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx new file mode 100644 index 0000000000..3fe3b60e25 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -0,0 +1,111 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityErrorFilter, EntityOrphanFilter } from '../../filters'; +import { + Box, + Checkbox, + FormControlLabel, + makeStyles, + TextField, + Typography, +} from '@material-ui/core'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import React, { useState } from 'react'; +import { useEntityList } from '../../hooks'; +import { Autocomplete } from '@material-ui/lab'; + +/** @public */ +export type CatalogReactEntityProcessingStatusPickerClassKey = 'input'; + +const useStyles = makeStyles( + { + input: {}, + }, + { + name: 'CatalogReactEntityProcessingStatusPickerPicker', + }, +); + +const icon = ; +const checkedIcon = ; + +/** @public */ +export const EntityProcessingStatusPicker = () => { + const classes = useStyles(); + const { updateFilters } = useEntityList(); + + const [selectedAdvancedItems, setSelectedAdvancedItems] = useState( + [], + ); + + function orphanChange(value: boolean) { + updateFilters({ + orphan: value ? new EntityOrphanFilter(value) : undefined, + }); + } + + function errorChange(value: boolean) { + updateFilters({ + error: value ? new EntityErrorFilter(value) : undefined, + }); + } + + const availableAdvancedItems = ['Is Orphan', 'Has Error']; + + return ( + + + Processing Status + { + setSelectedAdvancedItems(value); + orphanChange(value.includes('Is Orphan')); + errorChange(value.includes('Has Error')); + }} + renderOption={(option, { selected }) => ( + + } + label={option} + /> + )} + size="small" + popupIcon={ + + } + renderInput={params => ( + + )} + /> + + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/index.ts b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/index.ts new file mode 100644 index 0000000000..55daeaecc0 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +export { EntityProcessingStatusPicker as EntityProcessingStatusPicker } from './EntityProcessingStatusPicker'; +export type { CatalogReactEntityProcessingStatusPickerClassKey } from './EntityProcessingStatusPicker'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 7a26a72bf9..c604306ead 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -27,3 +27,4 @@ export * from './FavoriteEntity'; export * from './InspectEntityDialog'; export * from './UnregisterEntityDialog'; export * from './UserListPicker'; +export * from './EntityProcessingStatusPicker'; diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index ba31e21122..c2c92e033e 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { AlphaEntity, Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { EntityTextFilter } from './filters'; +import { + EntityErrorFilter, + EntityOrphanFilter, + EntityTextFilter, +} from './filters'; const entities: Entity[] = [ { @@ -91,3 +95,50 @@ describe('EntityTextFilter', () => { expect(filter.filterEntity(entities[1])).toBeTruthy(); }); }); + +describe('EntityOrphanFilter', () => { + const orphanAnnotation: Record = {}; + orphanAnnotation['backstage.io/orphan'] = 'true'; + + const orphan: Entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'orphaned-service', + annotations: orphanAnnotation, + }, + }; + + it('should find orphans', () => { + const filter = new EntityOrphanFilter(true); + expect(filter.filterEntity(orphan)).toBeTruthy(); + expect(filter.filterEntity(entities[1])).toBeFalsy(); + }); +}); + +describe('EntityErrorFilter', () => { + const error: AlphaEntity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'service-with-error', + tags: ['Invalid Tag'], + }, + status: { + items: [ + { + type: 'invalid-tag', + level: 'error', + message: 'Tag is not valid', + error: undefined, + }, + ], + }, + }; + + it('should find errors', () => { + const filter = new EntityErrorFilter(true); + expect(filter.filterEntity(error)).toBeTruthy(); + expect(filter.filterEntity(entities[1])).toBeFalsy(); + }); +}); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 9fb057a01c..a828015f0e 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + AlphaEntity, + Entity, + RELATION_OWNED_BY, +} from '@backstage/catalog-model'; import { humanizeEntityRef } from './components/EntityRefLink'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils'; @@ -159,3 +163,28 @@ export class UserListFilter implements EntityFilter { return this.value; } } + +/** + * Filters entities based if it is an orphan or not. + * @public + */ +export class EntityOrphanFilter implements EntityFilter { + constructor(readonly value: boolean) {} + filterEntity(entity: Entity): boolean { + const orphan = entity.metadata.annotations?.['backstage.io/orphan']; + return orphan !== undefined && this.value.toString() === orphan; + } +} + +/** + * Filters entities based on if it has errors or not. + * @public + */ +export class EntityErrorFilter implements EntityFilter { + constructor(readonly value: boolean) {} + filterEntity(entity: Entity): boolean { + const error = + ((entity as AlphaEntity)?.status?.items?.length as number) > 0; + return error !== undefined && this.value === error; + } +} diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index ccac475ae8..089701d8b4 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -31,8 +31,10 @@ import useDebounce from 'react-use/lib/useDebounce'; import useMountedState from 'react-use/lib/useMountedState'; import { catalogApiRef } from '../api'; import { + EntityErrorFilter, EntityKindFilter, EntityLifecycleFilter, + EntityOrphanFilter, EntityOwnerFilter, EntityTagFilter, EntityTextFilter, @@ -52,6 +54,8 @@ export type DefaultEntityFilters = { lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; text?: EntityTextFilter; + orphan?: EntityOrphanFilter; + error?: EntityErrorFilter; }; /** @public */ diff --git a/plugins/catalog-react/src/overridableComponents.ts b/plugins/catalog-react/src/overridableComponents.ts index 1e654988e4..c3b392fcfe 100644 --- a/plugins/catalog-react/src/overridableComponents.ts +++ b/plugins/catalog-react/src/overridableComponents.ts @@ -22,6 +22,7 @@ import { CatalogReactEntitySearchBarClassKey, CatalogReactEntityTagPickerClassKey, CatalogReactEntityOwnerPickerClassKey, + CatalogReactEntityProcessingStatusPickerClassKey, } from './components'; /** @public */ @@ -31,6 +32,7 @@ export type CatalogReactComponentsNameToClassKey = { CatalogReactEntitySearchBar: CatalogReactEntitySearchBarClassKey; CatalogReactEntityTagPicker: CatalogReactEntityTagPickerClassKey; CatalogReactEntityOwnerPicker: CatalogReactEntityOwnerPickerClassKey; + CatalogReactEntityProcessingStatusPicker: CatalogReactEntityProcessingStatusPickerClassKey; }; /** @public */ diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index 5250dcec0d..51ef12ac74 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -28,6 +28,7 @@ import { CatalogFilterLayout, EntityLifecyclePicker, EntityListProvider, + EntityProcessingStatusPicker, EntityOwnerPicker, EntityTagPicker, EntityTypePicker, @@ -84,6 +85,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { + ; + // (undocumented) + type: 'limitranges'; +} + // Warning: (ae-missing-release-tag) "ObjectsByEntityResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 68fbe09301..9b8b790c79 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -22,6 +22,7 @@ import { V1HorizontalPodAutoscaler, V1Ingress, V1Job, + V1LimitRange, V1Pod, V1ReplicaSet, V1Service, @@ -97,6 +98,7 @@ export type FetchResponse = | ServiceFetchResponse | ConfigMapFetchResponse | DeploymentFetchResponse + | LimitRangeFetchReponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse | JobsFetchResponse @@ -130,6 +132,11 @@ export interface ReplicaSetsFetchResponse { resources: Array; } +export interface LimitRangeFetchReponse { + type: 'limitranges'; + resources: Array; +} + export interface HorizontalPodAutoscalersFetchResponse { type: 'horizontalpodautoscalers'; resources: Array; diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 707b8e62ba..8d302e69f6 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -168,6 +168,62 @@ export type CreateGithubPullRequestClientFactoryInput = { token?: string; }; +// @public +export function createGithubRepoCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + access?: string | undefined; + deleteBranchOnMerge?: boolean | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + allowMergeCommit?: boolean | undefined; + requireCodeOwnerReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + repoVisibility?: 'internal' | 'private' | 'public' | undefined; + collaborators?: + | ( + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined; + token?: string | undefined; + topics?: string[] | undefined; +}>; + +// @public +export function createGithubRepoPushAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + protectDefaultBranch?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; + requireCodeOwnerReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; + // @public export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; @@ -345,6 +401,7 @@ export const createPublishGitlabMergeRequestAction: (options: { targetPath: string; token?: string | undefined; projectid?: string | undefined; + removeSourceBranch?: boolean | undefined; }>; // @public diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 5cb901d766..b94caf4fe8 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -59,7 +59,7 @@ "isbinaryfile": "^5.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "morgan": "^1.10.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 2ffbc09ce5..91f1e180d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -15,25 +15,34 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { JsonObject } from '@backstage/types'; import { CatalogApi } from '@backstage/catalog-client'; -import { - GithubCredentialsProvider, - ScmIntegrations, - DefaultGithubCredentialsProvider, -} from '@backstage/integration'; import { Config } from '@backstage/config'; import { - createCatalogWriteAction, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { JsonObject } from '@backstage/types'; +import { createCatalogRegisterAction, + createCatalogWriteAction, } from './catalog'; +import { TemplateFilter } from '../../../lib'; +import { TemplateAction } from '../types'; import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; import { createFilesystemDeleteAction, createFilesystemRenameAction, } from './filesystem'; +import { + createGithubActionsDispatchAction, + createGithubIssuesLabelAction, + createGithubRepoCreateAction, + createGithubRepoPushAction, + createGithubWebhookAction, +} from './github'; import { createPublishAzureAction, createPublishBitbucketAction, @@ -45,13 +54,6 @@ import { createPublishGitlabAction, createPublishGitlabMergeRequestAction, } from './publish'; -import { - createGithubActionsDispatchAction, - createGithubWebhookAction, - createGithubIssuesLabelAction, -} from './github'; -import { TemplateFilter } from '../../../lib'; -import { TemplateAction } from '../types'; /** * The options passed to {@link createBuiltinActions} @@ -165,6 +167,15 @@ export const createBuiltinActions = ( integrations, githubCredentialsProvider, }), + createGithubRepoCreateAction({ + integrations, + githubCredentialsProvider, + }), + createGithubRepoPushAction({ + integrations, + config, + githubCredentialsProvider, + }), ]; return actions as TemplateAction[]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts new file mode 100644 index 0000000000..0e7e907917 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -0,0 +1,416 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateAction } from '../../types'; + +jest.mock('../helpers'); + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { when } from 'jest-when'; +import { PassThrough } from 'stream'; +import { createGithubRepoCreateAction } from './githubRepoCreate'; + +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'description', + repoVisibility: 'private' as const, + access: 'owner/blam', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoCreateAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should call the githubApis with the correct values for createInOrg', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + await action.handler(mockContext); + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + visibility: 'private', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoVisibility: 'public', + }, + }); + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + visibility: 'public', + }); + }); + + it('should call the githubApis with the correct values for createForAuthenticatedUser', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: {}, + }); + + await action.handler(mockContext); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoVisibility: 'public', + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, + }); + }); + + it('should add access for the team when it starts with the owner', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg, + ).toHaveBeenCalledWith({ + org: 'owner', + team_slug: 'blam', + owner: 'owner', + repo: 'repo', + permission: 'admin', + }); + }); + + it('should add outside collaborators when provided', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + access: 'outsidecollaborator', + }, + }); + + expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({ + username: 'outsidecollaborator', + owner: 'owner', + repo: 'repo', + permission: 'admin', + }); + }); + + it('should add multiple collaborators when provided', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + collaborators: [ + { + access: 'pull', + user: 'robot-1', + }, + { + access: 'push', + team: 'robot-2', + }, + ], + }, + }); + + const commonProperties = { + owner: 'owner', + repo: 'repo', + }; + + expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({ + ...commonProperties, + username: 'robot-1', + permission: 'pull', + }); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg, + ).toHaveBeenCalledWith({ + ...commonProperties, + org: 'owner', + team_slug: 'robot-2', + permission: 'push', + }); + }); + + it('should ignore failures when adding multiple collaborators', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + when(mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg) + .calledWith({ + org: 'owner', + owner: 'owner', + repo: 'repo', + team_slug: 'robot-1', + permission: 'pull', + }) + .mockRejectedValueOnce(new Error('Something bad happened') as never); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + collaborators: [ + { + access: 'pull', + team: 'robot-1', + }, + { + access: 'push', + team: 'robot-2', + }, + ], + }, + }); + + expect( + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + ).toEqual([ + { + org: 'owner', + owner: 'owner', + repo: 'repo', + team_slug: 'robot-2', + permission: 'push', + }, + ]); + }); + + it('should add topics when provided', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['node.js'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['node.js'], + }, + }); + + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['node.js'], + }); + }); + + it('should lowercase topics when provided', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ + data: { + names: ['backstage'], + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + topics: ['BACKSTAGE'], + }, + }); + + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + names: ['backstage'], + }); + }); + + it('should call output with the remoteUrl', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts new file mode 100644 index 0000000000..1e92d91a64 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { createTemplateAction } from '../../createTemplateAction'; +import { parseRepoUrl } from '../publish/util'; +import { + createGithubRepoWithCollaboratorsAndTopics, + getOctokitOptions, +} from './helpers'; +import * as inputProps from './inputProperties'; +import * as outputProps from './outputProperties'; + +/** + * Creates a new action that initializes a git repository + * + * @public + */ +export function createGithubRepoCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + access?: string; + deleteBranchOnMerge?: boolean; + gitAuthorName?: string; + gitAuthorEmail?: string; + allowRebaseMerge?: boolean; + allowSquashMerge?: boolean; + allowMergeCommit?: boolean; + requireCodeOwnerReviews?: boolean; + requiredStatusCheckContexts?: string[]; + repoVisibility?: 'private' | 'internal' | 'public'; + collaborators?: Array< + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + /** @deprecated This field is deprecated in favor of team */ + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + >; + token?: string; + topics?: string[]; + }>({ + id: 'github:repo:create', + description: 'Creates a GitHub repository.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: inputProps.repoUrl, + description: inputProps.description, + access: inputProps.access, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + repoVisibility: inputProps.repoVisibility, + deleteBranchOnMerge: inputProps.deleteBranchOnMerge, + allowMergeCommit: inputProps.allowMergeCommit, + allowSquashMerge: inputProps.allowSquashMerge, + allowRebaseMerge: inputProps.allowRebaseMerge, + collaborators: inputProps.collaborators, + token: inputProps.token, + topics: inputProps.topics, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + access, + repoVisibility = 'private', + deleteBranchOnMerge = false, + allowMergeCommit = true, + allowSquashMerge = true, + allowRebaseMerge = true, + collaborators, + topics, + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + allowRebaseMerge, + access, + collaborators, + topics, + ctx.logger, + ); + + ctx.output('remoteUrl', newRepo.clone_url); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts new file mode 100644 index 0000000000..f23e3dde9f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.test.ts @@ -0,0 +1,400 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateAction } from '../../types'; + +jest.mock('../helpers'); + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; +import { createGithubRepoPushAction } from './githubRepoPush'; + +const mockOctokit = { + rest: { + repos: { + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:push', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + description: 'description', + repoVisibility: 'private' as const, + access: 'owner/blam', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoPushAction({ + integrations, + config, + githubCredentialsProvider, + }); + }); + + it('should call initRepoAndPush with the correct values', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initRepoAndPush with the correct defaultBranch main', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'main', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initRepoAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createGithubRepoPushAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + githubCredentialsProvider, + }); + + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + + it('should call initRepoAndPush with the configured defaultCommitMessage', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + scaffolder: { + defaultCommitMessage: 'Test commit message', + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createGithubRepoPushAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + githubCredentialsProvider, + }); + + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { email: undefined, name: undefined }, + }); + }); + + it('should call output with the remoteUrl and the repoContentsUrl', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://github.com/html/url/blob/master', + ); + }); + + it('should use main as default branch', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/clone/url.git', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://github.com/html/url/blob/main', + ); + }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: true, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: true, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredStatusCheckContexts', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: ['statusCheck'], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: ['statusCheck'], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: [], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + }); + + it('should not call enableBranchProtectionOnDefaultRepoBranch with protectDefaultBranch disabled', async () => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + protectDefaultBranch: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts new file mode 100644 index 0000000000..942ebe578d --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -0,0 +1,139 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { createTemplateAction } from '../../createTemplateAction'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; +import * as inputProps from './inputProperties'; +import * as outputProps from './outputProperties'; + +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to GitHub. + * + * @public + */ +export function createGithubRepoPushAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, config, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + defaultBranch?: string; + protectDefaultBranch?: boolean; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; + requireCodeOwnerReviews?: boolean; + requiredStatusCheckContexts?: string[]; + sourcePath?: string; + token?: string; + }>({ + id: 'github:repo:push', + description: + 'Initializes a git repository of contents in workspace and publishes it to GitHub.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: inputProps.repoUrl, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + defaultBranch: inputProps.defaultBranch, + protectDefaultBranch: inputProps.protectDefaultBranch, + gitCommitMessage: inputProps.gitCommitMessage, + gitAuthorName: inputProps.gitAuthorName, + gitAuthorEmail: inputProps.gitAuthorEmail, + sourcePath: inputProps.sourcePath, + token: inputProps.token, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + defaultBranch = 'master', + protectDefaultBranch = true, + gitCommitMessage = 'initial commit', + gitAuthorName, + gitAuthorEmail, + requireCodeOwnerReviews = false, + requiredStatusCheckContexts = [], + token: providedToken, + } = ctx.input; + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl, + }); + + const client = new Octokit(octokitOptions); + + const targetRepo = await client.rest.repos.get({ owner, repo }); + + const remoteUrl = targetRepo.data.clone_url; + const repoContentsUrl = `${targetRepo.data.html_url}/blob/${defaultBranch}`; + + await initRepoPushAndProtect( + remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, + defaultBranch, + protectDefaultBranch, + owner, + client, + repo, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + ); + + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index 29997e5843..c8f508e4c0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -13,14 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InputError } from '@backstage/errors'; +import { Config } from '@backstage/config'; +import { assertError, InputError } from '@backstage/errors'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { OctokitOptions } from '@octokit/core/dist-types/types'; -import { parseRepoUrl } from '../publish/util'; +import { Octokit } from 'octokit'; +import { Logger } from 'winston'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; +import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util'; const DEFAULT_TIMEOUT_MS = 60_000; @@ -83,3 +90,213 @@ export async function getOctokitOptions(options: { previews: ['nebula-preview'], }; } + +export async function createGithubRepoWithCollaboratorsAndTopics( + client: Octokit, + repo: string, + owner: string, + repoVisibility: 'private' | 'internal' | 'public', + description: string | undefined, + deleteBranchOnMerge: boolean, + allowMergeCommit: boolean, + allowSquashMerge: boolean, + allowRebaseMerge: boolean, + access: string | undefined, + collaborators: + | ( + | { + user: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + team: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + | { + /** @deprecated This field is deprecated in favor of team */ + username: string; + access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage'; + } + )[] + | undefined, + topics: string[] | undefined, + logger: Logger, +) { + const user = await client.rest.users.getByUsername({ + username: owner, + }); + + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + private: repoVisibility === 'private', + visibility: repoVisibility, + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + private: repoVisibility === 'private', + description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, + }); + + let newRepo; + + try { + newRepo = (await repoCreationPromise).data; + } catch (e) { + assertError(e); + if (e.message === 'Resource not accessible by integration') { + logger.warn( + `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, + ); + } + throw new Error( + `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, + ); + } + + if (access?.startsWith(`${owner}/`)) { + const [, team] = access.split('/'); + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: team, + owner, + repo, + permission: 'admin', + }); + // No need to add access if it's the person who owns the personal account + } else if (access && access !== owner) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: access, + permission: 'admin', + }); + } + + if (collaborators) { + for (const collaborator of collaborators) { + try { + if ('user' in collaborator) { + await client.rest.repos.addCollaborator({ + owner, + repo, + username: collaborator.user, + permission: collaborator.access, + }); + } else if ('team' in collaborator) { + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: collaborator.team, + owner, + repo, + permission: collaborator.access, + }); + } + } catch (e) { + assertError(e); + const name = extractCollaboratorName(collaborator); + logger.warn( + `Skipping ${collaborator.access} access for ${name}, ${e.message}`, + ); + } + } + } + + if (topics) { + try { + await client.rest.repos.replaceAllTopics({ + owner, + repo, + names: topics.map(t => t.toLowerCase()), + }); + } catch (e) { + assertError(e); + logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); + } + } + + return newRepo; +} + +export async function initRepoPushAndProtect( + remoteUrl: string, + password: string, + workspacePath: string, + sourcePath: string | undefined, + defaultBranch: string, + protectDefaultBranch: boolean, + owner: string, + client: Octokit, + repo: string, + requireCodeOwnerReviews: boolean, + requiredStatusCheckContexts: string[], + config: Config, + logger: any, + gitCommitMessage?: string, + gitAuthorName?: string, + gitAuthorEmail?: string, +) { + const gitAuthorInfo = { + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + const commitMessage = gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'); + + await initRepoAndPush({ + dir: getRepoSourceDirectory(workspacePath, sourcePath), + remoteUrl, + defaultBranch, + auth: { + username: 'x-access-token', + password, + }, + logger, + commitMessage, + gitAuthorInfo, + }); + + if (protectDefaultBranch) { + try { + await enableBranchProtectionOnDefaultRepoBranch({ + owner, + client, + repoName: repo, + logger, + defaultBranch, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + }); + } catch (e) { + assertError(e); + logger.warn( + `Skipping: default branch protection on '${repo}', ${e.message}`, + ); + } + } +} + +function extractCollaboratorName( + collaborator: { user: string } | { team: string } | { username: string }, +) { + if ('username' in collaborator) return collaborator.username; + if ('user' in collaborator) return collaborator.user; + return collaborator.team; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index c5788afc3a..6746aef37e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -15,5 +15,7 @@ */ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; -export { createGithubWebhookAction } from './githubWebhook'; export { createGithubIssuesLabelAction } from './githubIssuesLabel'; +export { createGithubRepoCreateAction } from './githubRepoCreate'; +export { createGithubRepoPushAction } from './githubRepoPush'; +export { createGithubWebhookAction } from './githubWebhook'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts new file mode 100644 index 0000000000..0aad402f1f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const repoUrl = { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', +}; +const description = { + title: 'Repository Description', + type: 'string', +}; +const access = { + title: 'Repository Access', + description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, + type: 'string', +}; +const requireCodeOwnerReviews = { + title: 'Require CODEOWNER Reviews?', + description: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', +}; +const requiredStatusCheckContexts = { + title: 'Required Status Check Contexts', + description: + 'The list of status checks to require in order to merge into this branch', + type: 'array', + items: { + type: 'string', + }, +}; +const repoVisibility = { + title: 'Repository Visibility', + type: 'string', + enum: ['private', 'public', 'internal'], +}; +const deleteBranchOnMerge = { + title: 'Delete Branch On Merge', + type: 'boolean', + description: `Delete the branch after merging the PR. The default value is 'false'`, +}; +const gitAuthorName = { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, +}; +const gitAuthorEmail = { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, +}; +const allowMergeCommit = { + title: 'Allow Merge Commits', + type: 'boolean', + description: `Allow merge commits. The default value is 'true'`, +}; +const allowSquashMerge = { + title: 'Allow Squash Merges', + type: 'boolean', + description: `Allow squash merges. The default value is 'true'`, +}; +const allowRebaseMerge = { + title: 'Allow Rebase Merges', + type: 'boolean', + description: `Allow rebase merges. The default value is 'true'`, +}; +const collaborators = { + title: 'Collaborators', + description: 'Provide additional users or teams with permissions', + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['access'], + properties: { + access: { + type: 'string', + description: 'The type of access for the user', + enum: ['push', 'pull', 'admin', 'maintain', 'triage'], + }, + user: { + type: 'string', + description: + 'The name of the user that will be added as a collaborator', + }, + team: { + type: 'string', + description: + 'The name of the team that will be added as a collaborator', + }, + }, + oneOf: [{ required: ['user'] }, { required: ['team'] }], + }, +}; +const token = { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', +}; +const topics = { + title: 'Topics', + type: 'array', + items: { + type: 'string', + }, +}; +const defaultBranch = { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, +}; +const protectDefaultBranch = { + title: 'Protect Default Branch', + type: 'boolean', + description: `Protect the default branch after creating the repository. The default value is 'true'`, +}; +const gitCommitMessage = { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, +}; +const sourcePath = { + title: 'Source Path', + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', + type: 'string', +}; + +export { access }; +export { allowMergeCommit }; +export { allowRebaseMerge }; +export { allowSquashMerge }; +export { collaborators }; +export { defaultBranch }; +export { deleteBranchOnMerge }; +export { description }; +export { gitAuthorEmail }; +export { gitAuthorName }; +export { gitCommitMessage }; +export { protectDefaultBranch }; +export { repoUrl }; +export { repoVisibility }; +export { requireCodeOwnerReviews }; +export { requiredStatusCheckContexts }; +export { sourcePath }; +export { token }; +export { topics }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts new file mode 100644 index 0000000000..d5d6eb108f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/outputProperties.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const remoteUrl = { + title: 'A URL to the repository with the provider', + type: 'string', +}; +const repoContentsUrl = { + title: 'A URL to the root of the repository', + type: 'string', +}; + +export { remoteUrl }; +export { repoContentsUrl }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 529a504cba..8753d44041 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; +import { Git } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { assertError } from '@backstage/errors'; +import { spawn, SpawnOptionsWithoutStdio } from 'child_process'; +import { Octokit } from 'octokit'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; -import { Git } from '@backstage/backend-common'; -import { Octokit } from 'octokit'; -import { assertError } from '@backstage/errors'; /** @public */ export type RunCommandOptions = { @@ -200,3 +201,12 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ await tryOnce(); } }; + +export function getGitCommitMessage( + gitCommitMessage: string | undefined, + config: Config, +): string | undefined { + return gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index ff20d1014b..036c5d66ee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -18,20 +18,20 @@ import { TemplateAction } from '../../types'; jest.mock('../helpers'); -import { createPublishGithubAction } from './github'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { - ScmIntegrations, DefaultGithubCredentialsProvider, GithubCredentialsProvider, + ScmIntegrations, } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; +import { when } from 'jest-when'; import { PassThrough } from 'stream'; import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, } from '../helpers'; -import { when } from 'jest-when'; +import { createPublishGithubAction } from './github'; const mockOctokit = { rest: { @@ -609,7 +609,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); @@ -618,7 +618,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -636,7 +636,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: true, @@ -654,7 +654,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -669,7 +669,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); @@ -678,7 +678,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -696,7 +696,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -714,7 +714,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', client: mockOctokit, - repoName: 'repository', + repoName: 'repo', logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, @@ -729,7 +729,7 @@ describe('publish:github', () => { mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { - name: 'repository', + name: 'repo', }, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index d9a7112fc9..0815a2cd00 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -13,21 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { - enableBranchProtectionOnDefaultRepoBranch, - initRepoAndPush, -} from '../helpers'; -import { getRepoSourceDirectory, parseRepoUrl } from './util'; -import { createTemplateAction } from '../../createTemplateAction'; -import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; -import { getOctokitOptions } from '../github/helpers'; import { Octokit } from 'octokit'; - +import { createTemplateAction } from '../../createTemplateAction'; +import { + createGithubRepoWithCollaboratorsAndTopics, + getOctokitOptions, + initRepoPushAndProtect, +} from '../github/helpers'; +import * as inputProps from '../github/inputProperties'; +import * as outputProps from '../github/outputProperties'; +import { parseRepoUrl } from './util'; /** * Creates a new action that initializes a git repository of the content in the workspace * and publishes it to GitHub. @@ -84,153 +85,32 @@ export function createPublishGithubAction(options: { type: 'object', required: ['repoUrl'], properties: { - repoUrl: { - title: 'Repository Location', - description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, - type: 'string', - }, - description: { - title: 'Repository Description', - type: 'string', - }, - access: { - title: 'Repository Access', - description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, - type: 'string', - }, - requireCodeOwnerReviews: { - title: 'Require CODEOWNER Reviews?', - description: - 'Require an approved review in PR including files with a designated Code Owner', - type: 'boolean', - }, - requiredStatusCheckContexts: { - title: 'Required Status Check Contexts', - description: - 'The list of status checks to require in order to merge into this branch', - type: 'array', - items: { - type: 'string', - }, - }, - repoVisibility: { - title: 'Repository Visibility', - type: 'string', - enum: ['private', 'public', 'internal'], - }, - defaultBranch: { - title: 'Default Branch', - type: 'string', - description: `Sets the default branch on the repository. The default value is 'master'`, - }, - protectDefaultBranch: { - title: 'Protect Default Branch', - type: 'boolean', - description: `Protect the default branch after creating the repository. The default value is 'true'`, - }, - deleteBranchOnMerge: { - title: 'Delete Branch On Merge', - type: 'boolean', - description: `Delete the branch after merging the PR. The default value is 'false'`, - }, - gitCommitMessage: { - title: 'Git Commit Message', - type: 'string', - description: `Sets the commit message on the repository. The default value is 'initial commit'`, - }, - gitAuthorName: { - title: 'Default Author Name', - type: 'string', - description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, - }, - gitAuthorEmail: { - title: 'Default Author Email', - type: 'string', - description: `Sets the default author email for the commit.`, - }, - allowMergeCommit: { - title: 'Allow Merge Commits', - type: 'boolean', - description: `Allow merge commits. The default value is 'true'`, - }, - allowSquashMerge: { - title: 'Allow Squash Merges', - type: 'boolean', - description: `Allow squash merges. The default value is 'true'`, - }, - allowRebaseMerge: { - title: 'Allow Rebase Merges', - type: 'boolean', - description: `Allow rebase merges. The default value is 'true'`, - }, - sourcePath: { - title: 'Source Path', - description: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', - type: 'string', - }, - collaborators: { - title: 'Collaborators', - description: 'Provide additional users or teams with permissions', - type: 'array', - items: { - type: 'object', - additionalProperties: false, - required: ['access'], - properties: { - access: { - type: 'string', - description: 'The type of access for the user', - enum: ['push', 'pull', 'admin', 'maintain', 'triage'], - }, - user: { - type: 'string', - description: - 'The name of the user that will be added as a collaborator', - }, - username: { - type: 'string', - description: - 'Deprecated. Use the `team` or `user` field instead.', - }, - team: { - type: 'string', - description: - 'The name of the team that will be added as a collaborator', - }, - }, - oneOf: [ - { required: ['user'] }, - { required: ['username'] }, - { required: ['team'] }, - ], - }, - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitHub', - }, - topics: { - title: 'Topics', - type: 'array', - items: { - type: 'string', - }, - }, + repoUrl: inputProps.repoUrl, + description: inputProps.description, + access: inputProps.access, + requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + repoVisibility: inputProps.repoVisibility, + defaultBranch: inputProps.defaultBranch, + protectDefaultBranch: inputProps.protectDefaultBranch, + deleteBranchOnMerge: inputProps.deleteBranchOnMerge, + gitCommitMessage: inputProps.gitCommitMessage, + gitAuthorName: inputProps.gitAuthorName, + gitAuthorEmail: inputProps.gitAuthorEmail, + allowMergeCommit: inputProps.allowMergeCommit, + allowSquashMerge: inputProps.allowSquashMerge, + allowRebaseMerge: inputProps.allowRebaseMerge, + sourcePath: inputProps.sourcePath, + collaborators: inputProps.collaborators, + token: inputProps.token, + topics: inputProps.topics, }, }, output: { type: 'object', properties: { - remoteUrl: { - title: 'A URL to the repository with the provider', - type: 'string', - }, - repoContentsUrl: { - title: 'A URL to the root of the repository', - type: 'string', - }, + remoteUrl: outputProps.remoteUrl, + repoContentsUrl: outputProps.repoContentsUrl, }, }, }, @@ -256,192 +136,60 @@ export function createPublishGithubAction(options: { token: providedToken, } = ctx.input; + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } - const octokitOptions = await getOctokitOptions({ - integrations, - credentialsProvider: githubCredentialsProvider, - token: providedToken, - repoUrl, - }); - - const client = new Octokit(octokitOptions); - - const user = await client.rest.users.getByUsername({ - username: owner, - }); - - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - private: repoVisibility === 'private', - visibility: repoVisibility, - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - private: repoVisibility === 'private', - description: description, - delete_branch_on_merge: deleteBranchOnMerge, - allow_merge_commit: allowMergeCommit, - allow_squash_merge: allowSquashMerge, - allow_rebase_merge: allowRebaseMerge, - }); - - let newRepo; - - try { - newRepo = (await repoCreationPromise).data; - } catch (e) { - assertError(e); - if (e.message === 'Resource not accessible by integration') { - ctx.logger.warn( - `The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`, - ); - } - throw new Error( - `Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`, - ); - } - - if (access?.startsWith(`${owner}/`)) { - const [, team] = access.split('/'); - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: team, - owner, - repo, - permission: 'admin', - }); - // No need to add access if it's the person who owns the personal account - } else if (access && access !== owner) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: access, - permission: 'admin', - }); - } - - if (collaborators) { - for (const collaborator of collaborators) { - try { - if ('user' in collaborator) { - await client.rest.repos.addCollaborator({ - owner, - repo, - username: collaborator.user, - permission: collaborator.access, - }); - } else if ('username' in collaborator) { - ctx.logger.warn( - 'The field `username` is deprecated in favor of `team` and will be removed in the future.', - ); - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: collaborator.username, - owner, - repo, - permission: collaborator.access, - }); - } else if ('team' in collaborator) { - await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: collaborator.team, - owner, - repo, - permission: collaborator.access, - }); - } - } catch (e) { - assertError(e); - const name = extractCollaboratorName(collaborator); - ctx.logger.warn( - `Skipping ${collaborator.access} access for ${name}, ${e.message}`, - ); - } - } - } - - if (topics) { - try { - await client.rest.repos.replaceAllTopics({ - owner, - repo, - names: topics.map(t => t.toLowerCase()), - }); - } catch (e) { - assertError(e); - ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`); - } - } + const newRepo = await createGithubRepoWithCollaboratorsAndTopics( + client, + repo, + owner, + repoVisibility, + description, + deleteBranchOnMerge, + allowMergeCommit, + allowSquashMerge, + allowRebaseMerge, + access, + collaborators, + topics, + ctx.logger, + ); const remoteUrl = newRepo.clone_url; const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; - const gitAuthorInfo = { - name: gitAuthorName - ? gitAuthorName - : config.getOptionalString('scaffolder.defaultAuthor.name'), - email: gitAuthorEmail - ? gitAuthorEmail - : config.getOptionalString('scaffolder.defaultAuthor.email'), - }; - - await initRepoAndPush({ - dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + await initRepoPushAndProtect( remoteUrl, + octokitOptions.auth, + ctx.workspacePath, + ctx.input.sourcePath, defaultBranch, - auth: { - username: 'x-access-token', - password: octokitOptions.auth, - }, - logger: ctx.logger, - commitMessage: gitCommitMessage - ? gitCommitMessage - : config.getOptionalString('scaffolder.defaultCommitMessage'), - gitAuthorInfo, - }); - - if (protectDefaultBranch) { - try { - await enableBranchProtectionOnDefaultRepoBranch({ - owner, - client, - repoName: newRepo.name, - logger: ctx.logger, - defaultBranch, - requireCodeOwnerReviews, - requiredStatusCheckContexts, - }); - } catch (e) { - assertError(e); - ctx.logger.warn( - `Skipping: default branch protection on '${newRepo.name}', ${e.message}`, - ); - } - } + protectDefaultBranch, + owner, + client, + repo, + requireCodeOwnerReviews, + requiredStatusCheckContexts, + config, + ctx.logger, + gitCommitMessage, + gitAuthorName, + gitAuthorEmail, + ); ctx.output('remoteUrl', remoteUrl); ctx.output('repoContentsUrl', repoContentsUrl); }, }); } - -function extractCollaboratorName( - collaborator: { user: string } | { team: string } | { username: string }, -) { - if ('username' in collaborator) return collaborator.username; - if ('user' in collaborator) return collaborator.user; - return collaborator.team; -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts new file mode 100644 index 0000000000..987e360617 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -0,0 +1,207 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getRootLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { Writable } from 'stream'; +import { TemplateAction } from '../../types'; +import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; + +const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; +const workspacePath = resolvePath(root, 'my-workspace'); + +const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Branches: { + create: jest.fn(), + }, + Commits: { + create: jest.fn(), + }, + MergeRequests: { + create: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + Projects: { + create: jest.fn(), + show: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + Users: { + current: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('createGitLabMergeRequest', () => { + let instance: TemplateAction; + + beforeEach(() => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + instance = createPublishGitlabMergeRequestAction({ integrations }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe('createGitLabMergeRequestWithoutRemoveBranch', () => { + it('removeSourceBranch is false by default when not passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This MR is really good', + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { description: 'This MR is really good', removeSourceBranch: false }, + ); + }); + }); + + describe('createGitLabMergeRequestWithRemoveBranch', () => { + it('removeSourceBranch is true when true is passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + removeSourceBranch: true, + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { description: 'MR description', removeSourceBranch: true }, + ); + }); + + it('removeSourceBranch is false when false is passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'other MR description', + removeSourceBranch: false, + draft: true, + targetPath: 'Subdirectory', + }; + mockFs({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = { + createTemporaryDirectory: jest.fn(), + output: jest.fn(), + logger: getRootLogger(), + logStream: new Writable(), + input, + workspacePath, + }; + await instance.handler(ctx); + + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'other MR description', + removeSourceBranch: false, + }, + ); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index f717a7a0c6..b1d5b35fa3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -42,6 +42,7 @@ export const createPublishGitlabMergeRequestAction = (options: { token?: string; /** @deprecated Use projectPath instead */ projectid?: string; + removeSourceBranch?: boolean; }>({ id: 'publish:gitlab:merge-request', schema: { @@ -85,6 +86,12 @@ export const createPublishGitlabMergeRequestAction = (options: { type: 'string', description: 'The token to use for authorization to GitLab', }, + removeSourceBranch: { + title: 'Delete source branch', + type: 'boolean', + description: + 'Option to delete source branch once the MR has been merged. Default: false', + }, }, }, output: { @@ -187,7 +194,12 @@ export const createPublishGitlabMergeRequestAction = (options: { destinationBranch, String(defaultBranch), ctx.input.title, - { description: ctx.input.description }, + { + description: ctx.input.description, + removeSourceBranch: ctx.input.removeSourceBranch + ? ctx.input.removeSourceBranch + : false, + }, ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 9ffca426ac..3fa623b5b9 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -153,6 +153,8 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< // @public export interface OwnedEntityPickerUiOptions { + // (undocumented) + allowArbitraryValues?: boolean; // (undocumented) allowedKinds?: string[]; // (undocumented) @@ -193,6 +195,8 @@ export interface RepoUrlPickerUiOptions { // (undocumented) allowedOwners?: string[]; // (undocumented) + allowedRepos?: string[]; + // (undocumented) requestUserCredentials?: { secretsKey: string; additionalScopes?: { diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 8e3590e8bf..a280ebad6a 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -37,6 +37,7 @@ import { FieldExtensionComponentProps } from '../../../extensions'; export interface OwnedEntityPickerUiOptions { allowedKinds?: string[]; defaultKind?: string; + allowArbitraryValues?: boolean; } /** @@ -60,6 +61,8 @@ export const OwnedEntityPicker = ( const allowedKinds = uiSchema['ui:options']?.allowedKinds; const defaultKind = uiSchema['ui:options']?.defaultKind; + const allowArbitraryValues = + uiSchema['ui:options']?.allowArbitraryValues ?? true; const { ownedEntities, loading } = useOwnedEntities(allowedKinds); const entityRefs = ownedEntities?.items @@ -83,7 +86,7 @@ export const OwnedEntityPicker = ( onChange={onSelect} options={entityRefs || []} autoSelect - freeSolo + freeSolo={allowArbitraryValues} renderInput={params => ( { - it('renders the three input fields', async () => { + it('renders the two input fields', async () => { const { getAllByRole } = render( , ); const allInputs = getAllByRole('textbox'); - expect(allInputs).toHaveLength(3); + expect(allInputs).toHaveLength(2); }); describe('org field', () => { @@ -58,19 +58,4 @@ describe('AzureRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ owner: 'owner' }); }); }); - - describe('repoName field', () => { - it('calls onChange when the repoName changes', () => { - const onChange = jest.fn(); - const { getAllByRole } = render( - , - ); - - const repoNameInput = getAllByRole('textbox')[2]; - - fireEvent.change(repoNameInput, { target: { value: 'repoName' } }); - - expect(onChange).toHaveBeenCalledWith({ repoName: 'repoName' }); - }); - }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 0efadf4017..0246c67801 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -27,7 +27,7 @@ export const AzureRepoPicker = (props: { rawErrors: string[]; }) => { const { rawErrors, state, onChange } = props; - const { organization, repoName, owner } = state; + const { organization, owner } = state; return ( <> The Owner that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx index 17fe656006..0ced19a596 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -26,7 +26,7 @@ describe('BitbucketRepoPicker', () => { , ); - expect(getAllByRole('textbox')).toHaveLength(3); + expect(getAllByRole('textbox')).toHaveLength(2); expect(getAllByRole('textbox')[0]).toHaveValue('lolsWorkspace'); }); @@ -39,7 +39,7 @@ describe('BitbucketRepoPicker', () => { , ); - expect(getAllByRole('textbox')).toHaveLength(2); + expect(getAllByRole('textbox')).toHaveLength(1); }); describe('workspace field', () => { it('calls onChange when the workspace changes', () => { @@ -78,23 +78,4 @@ describe('BitbucketRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ project: 'test-project' }); }); }); - - describe('repoName field', () => { - it('calls onChange when the repoName changes', () => { - const onChange = jest.fn(); - const { getAllByRole } = render( - , - ); - - const repoNameInput = getAllByRole('textbox')[2]; - - fireEvent.change(repoNameInput, { target: { value: 'test-repo' } }); - - expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' }); - }); - }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 64eb4b8dac..fc59398d94 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -26,7 +26,7 @@ export const BitbucketRepoPicker = (props: { rawErrors: string[]; }) => { const { onChange, rawErrors, state } = props; - const { host, workspace, project, repoName } = state; + const { host, workspace, project } = state; return ( <> {host === 'bitbucket.org' && ( @@ -61,19 +61,6 @@ export const BitbucketRepoPicker = (props: { The Project that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx index edc6732588..66f0a234be 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx @@ -56,23 +56,4 @@ describe('BitbucketRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ workspace: 'test-parent' }); }); }); - - describe('repoName field', () => { - it('calls onChange when the repoName changes', () => { - const onChange = jest.fn(); - const { getAllByRole } = render( - , - ); - - const repoNameInput = getAllByRole('textbox')[2]; - - fireEvent.change(repoNameInput, { target: { value: 'test-repo' } }); - - expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' }); - }); - }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx index 4cbc0859e8..f021948532 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx @@ -26,7 +26,7 @@ export const GerritRepoPicker = (props: { rawErrors: string[]; }) => { const { onChange, rawErrors, state } = props; - const { workspace, repoName, owner } = state; + const { workspace, owner } = state; return ( <> - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 2c80c40c22..63bc5b8013 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -84,24 +84,4 @@ describe('GithubRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); }); - - describe('repo name', () => { - it('should render free text field for input of repo name', () => { - const onChange = jest.fn(); - const { getAllByRole } = render( - , - ); - - const repoNameField = getAllByRole('textbox')[1]; - fireEvent.change(repoNameField, { - target: { value: 'my-mock-repo-name' }, - }); - - expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' }); - }); - }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 13b57cb449..ec59d89ef8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -32,7 +32,7 @@ export const GithubRepoPicker = (props: { ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner, repoName } = state; + const { owner } = state; return ( <> @@ -66,19 +66,6 @@ export const GithubRepoPicker = (props: { The organization, user or project that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx index 49a91c116e..ad5c7f765e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx @@ -84,24 +84,4 @@ describe('GitlabRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); }); - - describe('repo name', () => { - it('should render free text field for input of repo name', () => { - const onChange = jest.fn(); - const { getAllByRole } = render( - , - ); - - const repoNameField = getAllByRole('textbox')[1]; - fireEvent.change(repoNameField, { - target: { value: 'my-mock-repo-name' }, - }); - - expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' }); - }); - }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index e2052b792f..c628b364b9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -23,16 +23,17 @@ import { RepoUrlPickerState } from './types'; export const GitlabRepoPicker = (props: { allowedOwners?: string[]; + allowedRepos?: string[]; state: RepoUrlPickerState; onChange: (state: RepoUrlPickerState) => void; rawErrors: string[]; }) => { - const { allowedOwners = [], rawErrors, state, onChange } = props; + const { allowedOwners = [], state, onChange, rawErrors } = props; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner, repoName } = state; + const { owner } = state; return ( <> @@ -69,19 +70,6 @@ export const GitlabRepoPicker = (props: { namespaces in gitlab), that this repo will belong to - 0 && !repoName} - > - Repository - onChange({ repoName: e.target.value })} - value={repoName} - /> - The name of the repository - ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 6f762933b6..b36b332d9e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -26,6 +26,7 @@ import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { GerritRepoPicker } from './GerritRepoPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; +import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; @@ -40,6 +41,7 @@ import { useTemplateSecrets } from '../../secrets'; export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; allowedOwners?: string[]; + allowedRepos?: string[]; requestUserCredentials?: { secretsKey: string; additionalScopes?: { @@ -76,6 +78,10 @@ export const RepoUrlPicker = ( () => uiSchema?.['ui:options']?.allowedOwners ?? [], [uiSchema], ); + const allowedRepos = useMemo( + () => uiSchema?.['ui:options']?.allowedRepos ?? [], + [uiSchema], + ); useEffect(() => { onChange(serializeRepoPickerUrl(state)); @@ -87,6 +93,11 @@ export const RepoUrlPicker = ( setState(prevState => ({ ...prevState, owner: allowedOwners[0] })); } }, [setState, allowedOwners]); + useEffect(() => { + if (allowedRepos.length === 1) { + setState(prevState => ({ ...prevState, repoName: allowedRepos[0] })); + } + }, [setState, allowedRepos]); const updateLocalState = useCallback( (newState: RepoUrlPickerState) => { @@ -179,6 +190,14 @@ export const RepoUrlPicker = ( onChange={updateLocalState} /> )} + + setState(prevState => ({ ...prevState, repoName })) + } + rawErrors={rawErrors} + /> ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx new file mode 100644 index 0000000000..4c392d646c --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; +import { render, fireEvent } from '@testing-library/react'; + +describe('RepoUrlPickerRepoName', () => { + it('should call onChange with the first allowed repo if there is none set already', async () => { + const onChange = jest.fn(); + + render( + , + ); + + expect(onChange).toHaveBeenCalledWith('foo'); + }); + + it('should render a dropdown of all the options', async () => { + const allowedRepos = ['foo', 'bar']; + + const onChange = jest.fn(); + + const { getByRole } = render( + , + ); + + const select = getByRole('combobox'); + await fireEvent.click(select); + + for (const option of allowedRepos) { + const element = getByRole('option', { name: option }); + expect(element).toBeVisible(); + } + }); + + it('should render a normal text area when no options are passed', async () => { + const onChange = jest.fn(); + + const { getByRole } = render( + , + ); + + const textArea = getByRole('textbox'); + + expect(textArea).toBeVisible(); + + fireEvent.change(textArea, { target: { value: 'foo' } }); + + expect(onChange).toHaveBeenCalledWith('foo'); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx new file mode 100644 index 0000000000..1393b1902e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect } from 'react'; +import { Select, SelectItem } from '@backstage/core-components'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; + +export const RepoUrlPickerRepoName = (props: { + repoName?: string; + allowedRepos?: string[]; + onChange: (host: string) => void; + rawErrors: string[]; +}) => { + const { repoName, allowedRepos, onChange, rawErrors } = props; + + useEffect(() => { + // If there is no repoName chosen currently + if (!repoName) { + // Set the first of the allowedRepos option if that available + if (allowedRepos?.length) { + onChange(allowedRepos[0]); + } + } + }, [allowedRepos, repoName, onChange]); + + const repoItems: SelectItem[] = allowedRepos + ? allowedRepos.map(i => ({ label: i, value: i })) + : [{ label: 'Loading...', value: 'loading' }]; + + return ( + <> + 0 && !repoName} + > + {allowedRepos?.length ? ( + onChange(String(e.target.value))} + value={repoName} + /> + + )} + The name of the repository + + + ); +}; diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 35d3d841f3..fb8cd4afc4 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -27,7 +27,7 @@ "@backstage/plugin-search-backend-node": "^0.6.3-next.1", "@backstage/plugin-search-common": "^0.3.6-next.0", "lodash": "^4.17.21", - "knex": "^1.0.2" + "knex": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.26-next.1", diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index a5e180e715..6ac9054a99 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -45,7 +45,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", "semver": "^7.3.5", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 5a96f3ab42..c316a93f76 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -49,7 +49,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", - "knex": "^1.0.2", + "knex": "^2.0.0", "lodash": "^4.17.21", "node-fetch": "^2.6.7", "p-limit": "^3.1.0", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 6885957002..9f69dcb8d8 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -106,16 +106,18 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { return ( {({ metadata, entityMetadata, onReady }) => ( - - {children instanceof Function - ? children({ - entityRef, - techdocsMetadataValue: metadata.value, - entityMetadataValue: entityMetadata.value, - onReady, - }) - : children} - +
+ + {children instanceof Function + ? children({ + entityRef, + techdocsMetadataValue: metadata.value, + entityMetadataValue: entityMetadata.value, + onReady, + }) + : children} + +
)}
); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 9f561bfcd3..76b07c9d32 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -81,9 +81,17 @@ export const useTechDocsReaderDom = ( if (isMobileMedia) { element.style.top = '0px'; } else { - const domTop = dom.getBoundingClientRect().top ?? 0; + const page = document?.querySelector('.techdocs-reader-page'); + const pageTop = page?.getBoundingClientRect().top ?? 0; + let domTop = dom.getBoundingClientRect().top ?? 0; + const tabs = dom.querySelector('.md-container > .md-tabs'); const tabsHeight = tabs?.getBoundingClientRect().height ?? 0; + + // the sidebars should not scroll beyond the total height of the header and tabs + if (domTop < pageTop) { + domTop = pageTop; + } element.style.top = `${Math.max(domTop, 0) + tabsHeight}px`; } diff --git a/plugins/vault-backend/api-report.md b/plugins/vault-backend/api-report.md index 9f738b15e9..d2e9e1b177 100644 --- a/plugins/vault-backend/api-report.md +++ b/plugins/vault-backend/api-report.md @@ -70,6 +70,7 @@ export interface VaultEnvironment { // @public export type VaultSecret = { name: string; + path: string; showUrl: string; editUrl: string; }; diff --git a/plugins/vault-backend/src/service/vaultApi.test.ts b/plugins/vault-backend/src/service/vaultApi.test.ts index e80703c99f..1fb869ef60 100644 --- a/plugins/vault-backend/src/service/vaultApi.test.ts +++ b/plugins/vault-backend/src/service/vaultApi.test.ts @@ -46,11 +46,13 @@ describe('VaultApi', () => { const mockSecretsResult: VaultSecret[] = [ { name: 'secret::one', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`, }, { name: 'secret::two', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`, }, diff --git a/plugins/vault-backend/src/service/vaultApi.ts b/plugins/vault-backend/src/service/vaultApi.ts index 14607a694f..158778294a 100644 --- a/plugins/vault-backend/src/service/vaultApi.ts +++ b/plugins/vault-backend/src/service/vaultApi.ts @@ -36,6 +36,7 @@ export type VaultSecretList = { */ export type VaultSecret = { name: string; + path: string; showUrl: string; editUrl: string; }; @@ -134,6 +135,7 @@ export class VaultClient implements VaultApi { } else { secrets.push({ name: secret, + path: secretPath, editUrl: `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/edit/${secretPath}/${secret}`, showUrl: `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/show/${secretPath}/${secret}`, }); diff --git a/plugins/vault/api-report.md b/plugins/vault/api-report.md index f6fa8d1269..b347989800 100644 --- a/plugins/vault/api-report.md +++ b/plugins/vault/api-report.md @@ -32,6 +32,7 @@ export const vaultPlugin: BackstagePlugin<{}, {}>; // @public export type VaultSecret = { name: string; + path: string; showUrl: string; editUrl: string; }; diff --git a/plugins/vault/dev/index.tsx b/plugins/vault/dev/index.tsx index f5056cd057..8d902e14c5 100644 --- a/plugins/vault/dev/index.tsx +++ b/plugins/vault/dev/index.tsx @@ -45,11 +45,13 @@ const mockedApi: VaultApi = { return [ { name: 'a::b', + path: '', editUrl: 'https://example.com', showUrl: 'https://example.com', }, { name: 'c::d', + path: '', editUrl: 'https://example.com', showUrl: 'https://example.com', }, diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 3733d8be0f..4ce6f95c41 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -31,11 +31,13 @@ describe('api', () => { items: [ { name: 'secret::one', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`, }, { name: 'secret::two', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`, }, diff --git a/plugins/vault/src/api.ts b/plugins/vault/src/api.ts index 6c17e34d9a..bdd881e08c 100644 --- a/plugins/vault/src/api.ts +++ b/plugins/vault/src/api.ts @@ -29,6 +29,7 @@ export const vaultApiRef = createApiRef({ */ export type VaultSecret = { name: string; + path: string; showUrl: string; editUrl: string; }; diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index f3d7748b3e..82a90f23e1 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -72,11 +72,13 @@ describe('EntityVaultTable', () => { items: [ { name: 'secret::one', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`, }, { name: 'secret::two', + path: 'test/success', editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`, showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`, }, diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx index 8311dea2aa..efc345b07f 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx @@ -53,12 +53,16 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => { ]; const data = (value || []).map(secret => { + const secretName = `${secret.path.replace(`${secretPath}/`, '')}/${ + secret.name + }`; + return { - secret: secret.name, + secret: secretName, view: ( @@ -67,7 +71,7 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => { edit: ( diff --git a/yarn.lock b/yarn.lock index a4d732961e..98689d74a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1457,7 +1457,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.17.7" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825" integrity sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA== @@ -1471,6 +1471,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.18.3": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" + integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -2305,10 +2312,10 @@ arrify "^2.0.0" extend "^3.0.2" -"@google-cloud/projectify@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" - integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== +"@google-cloud/projectify@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" + integrity sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA== "@google-cloud/promisify@^3.0.0": version "3.0.0" @@ -2316,12 +2323,12 @@ integrity sha512-91ArYvRgXWb73YvEOBMmOcJc0bDRs5yiVHnqkwoG0f3nm7nZuipllz6e7BvFESBvjkDTBC0zMD8QxedUwNLc1A== "@google-cloud/storage@^6.0.0": - version "6.1.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.1.0.tgz#f882a969c5637ff445764d7b172f68fd0d9e63f8" - integrity sha512-zqZwzpRWCJuPne7x9Vc2H79zANl0uh9bNPGis0xAuC88ZEvBXfQqYCAVyiL1YIxi7rf51l8wy9vBr1pONMfxxA== + version "6.2.2" + resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.2.2.tgz#1fd3ee85e5fea55a9696c98769d097b28ff4d775" + integrity sha512-KhAOxmGfmELKKn6cdvgGfAi/YBLi19hI1jX3QI7xQmbeajSFMgUKrIPbbyfMIxQPOEQ9vG0MQX1uganlA/HTRA== dependencies: "@google-cloud/paginator" "^3.0.7" - "@google-cloud/projectify" "^2.0.0" + "@google-cloud/projectify" "^3.0.0" "@google-cloud/promisify" "^3.0.0" abort-controller "^3.0.0" arrify "^2.0.0" @@ -2364,9 +2371,9 @@ meros "^1.1.4" "@graphql-codegen/cli@^2.3.1": - version "2.6.2" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.6.2.tgz#a9aa4656141ee0998cae8c7ad7d0bf9ca8e0c9ae" - integrity sha512-UO75msoVgvLEvfjCezM09cQQqp32+mR8Ma1ACsBpr7nroFvHbgcu2ulx1cMovg4sxDBCsvd9Eq/xOOMpARUxtw== + version "2.6.3" + resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.6.3.tgz#80d6f0324bcf9a375b9a1d5e998fcd62b3bace66" + integrity sha512-gxtKbe6LlBhGqDISZldCMcGaV1rSTS4D836clSR52kBY7UBhZZcDOcNZM0zDJDFGpjHtwuL9p0Kb4w9HRXYfew== dependencies: "@graphql-codegen/core" "2.5.1" "@graphql-codegen/plugin-helpers" "^2.4.1" @@ -2387,24 +2394,18 @@ common-tags "^1.8.0" cosmiconfig "^7.0.0" debounce "^1.2.0" - dependency-graph "^0.11.0" detect-indent "^6.0.0" - glob "^7.1.6" - globby "^11.0.4" graphql-config "^4.1.0" inquirer "^8.0.0" is-glob "^4.0.1" json-to-pretty-yaml "^1.2.2" - latest-version "5.1.0" + latest-version "^6.0.0" listr "^0.14.3" listr-update-renderer "^0.5.0" log-symbols "^4.0.0" - minimatch "^4.0.0" mkdirp "^1.0.4" string-env-interpolation "^1.0.1" ts-log "^2.2.3" - tslib "~2.3.0" - valid-url "^1.0.9" wrap-ansi "^7.0.0" yaml "^1.10.0" yargs "^17.0.0" @@ -4358,9 +4359,9 @@ react-is "^16.8.0 || ^17.0.0" "@maxim_mazurok/gapi.client.calendar@^3.0.20220408": - version "3.0.20220617" - resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220617.tgz#140bbfd2caa1770fedbdaec4182eb43684f2d876" - integrity sha512-Vek5Y655GUi4RmUs3cNLfo/KQeReEuvcViJ0KYHl28KmC2Pqmxb4V2YIUhsH7BGVyq84cTIT59qHSirB919Prw== + version "3.0.20220624" + resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220624.tgz#17817142e348ce811415dfb111299fbdac2450a0" + integrity sha512-5+x4A6l8GY+dojvXUBQGc0Y4JOm9lBY1YZfCAEEZPjBcXkx0vs/CenPnmAu/fwQgGSrQLipxFV+pS6RL3L28mg== dependencies: "@types/gapi.client" "*" @@ -5382,9 +5383,9 @@ react-use "^17.2.4" "@rollup/plugin-commonjs@^22.0.0": - version "22.0.0" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.0.tgz#f4d87016e2fbf187a593ab9f46626fe05b59e8bd" - integrity sha512-Ktvf2j+bAO+30awhbYoCaXpBcyPmJbaEUYClQns/+6SNCYFURbvBiNbWgHITEsIgDDWCDUclWRKEuf8cwZCFoQ== + version "22.0.1" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.1.tgz#f7cb777d20de3eeeaf994f39080115c336bef810" + integrity sha512-dGfEZvdjDHObBiP5IvwTKMVeq/tBZGMBHZFMdIV1ClMM/YoWS34xrHFGfag9SN2ZtMgNZRFruqvxZQEa70O6nQ== dependencies: "@rollup/pluginutils" "^3.1.0" commondir "^1.0.1" @@ -6212,7 +6213,7 @@ "@types/eslint" "*" "@types/estree" "*" -"@types/eslint@*", "@types/eslint@^7.28.2": +"@types/eslint@*": version "7.29.0" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== @@ -6220,6 +6221,14 @@ "@types/estree" "*" "@types/json-schema" "*" +"@types/eslint@^7.29.0 || ^8.4.1": + version "8.4.3" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.3.tgz#5c92815a3838b1985c90034cd85f26f59d9d0ece" + integrity sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + "@types/estree@*", "@types/estree@^0.0.51": version "0.0.51" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" @@ -6653,9 +6662,9 @@ integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/node@^16.0.0", "@types/node@^16.11.26", "@types/node@^16.9.2": - version "16.11.41" - resolved "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz#88eb485b1bfdb4c224d878b7832239536aa2f813" - integrity sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ== + version "16.11.42" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.42.tgz#d2a75c58e9b0902b82dc54bd4c13f8ef12bd1020" + integrity sha512-iwLrPOopPy6V3E+1yHTpJea3bdsNso0b0utLOJJwaa/PLzqBt3GZl3stMcakc/gr89SfcNk2ki3z7Gvue9hYGQ== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -7408,9 +7417,9 @@ eslint-visitor-keys "^3.0.0" "@uiw/react-codemirror@^4.9.3": - version "4.9.4" - resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.4.tgz#4643201f279fed103f2cf15df9431931fe4e9f15" - integrity sha512-IsC5xDevpIeLMzHQQwT2W40gFFIdKeT1T0DHjzzai+s5SIrMlGe3QSHWeC1wSO7FtfNxFpFlTYMGJm5JwUviMA== + version "4.9.5" + resolved "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.9.5.tgz#0f66c09dfc355baef5a7020f206da8db90bae223" + integrity sha512-KHgP/PII9Gv4iEUzbdO95qpSSPy27iSHzxQ01mBPWC4UvzuA0eQY2h64gzi/ld68esbKMGYoevgOBPCRJwNN1A== dependencies: "@babel/runtime" ">=7.11.0" "@codemirror/theme-one-dark" "^6.0.0" @@ -7563,9 +7572,9 @@ integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== "@yarnpkg/parsers@^3.0.0-rc.4": - version "3.0.0-rc.9" - resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.9.tgz#2d284e4e0c79b1c4e410465e217fa303d98b7be3" - integrity sha512-JMBE+6OJoNN9AXBzZ72u22/t9M25K8KgUWZIjvk8CU/NsE/m946l8D7SqMhbi3ZaUfYa5QitqCqVWTTFtysGJQ== + version "3.0.0-rc.10" + resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.10.tgz#cbbb9970d396fd274911ef0f8204d52132394271" + integrity sha512-TZxyre59hLjWQ0FcakvEl2a+PTArDJ2VYC3e3xjec+OpTuQSK7333VscpiT2UJKJPDUGrYq1Z1LDpnIYv4nHLw== dependencies: js-yaml "^3.10.0" tslib "^1.13.0" @@ -8118,7 +8127,7 @@ array-ify@^1.0.0: resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.1.2, array-includes@^3.1.3, array-includes@^3.1.4: +array-includes@^3.1.2, array-includes@^3.1.4: version "3.1.4" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== @@ -8335,9 +8344,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.1122.0, aws-sdk@^2.840.0, aws-sdk@^2.948.0: - version "2.1159.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1159.0.tgz#64d585ac608d58e6ff62678be71a4c4ca61ca5dc" - integrity sha512-zm3k/ufwZnkWc6M+HDz00CWuILot4L9kJ5VJsuDS9fwsT9To6k91Y1njCtIV4tcgcXvUru0Sbm4D0w5bc2847A== + version "2.1165.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1165.0.tgz#4da669d1e9020344cef75d961882f52a7931a379" + integrity sha512-2oVkSuXsLeErt+H4M2OGIz4p1LPS+QRfY2WnW4QKMndASOcvHKZTfzuY8jmc9ZnDGyguiGdT3idYU8KpNg0sGw== dependencies: buffer "4.9.2" events "1.1.1" @@ -8359,10 +8368,10 @@ aws4@^1.11.0, aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axe-core@^4.3.5: - version "4.3.5" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" - integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +axe-core@^4.4.2: + version "4.4.2" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c" + integrity sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA== axios-cached-dns-resolve@0.5.2: version "0.5.2" @@ -10148,10 +10157,10 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== consola@^2.15.0: version "2.15.3" @@ -10360,9 +10369,9 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.4.1, core-js@^3.6.5: - version "3.23.2" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.23.2.tgz#e07a60ca8b14dd129cabdc3d2551baf5a01c76f0" - integrity sha512-ELJOWxNrJfOH/WK4VJ3Qd+fOqZuOuDNDJz0xG6Bt4mGg2eO/UT9CljCrbqDGovjLKUrGajEEBcoTOc0w+yBYeQ== + version "3.23.3" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112" + integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -10478,9 +10487,9 @@ cron@^2.0.0: luxon "^1.23.x" cronstrue@^2.2.0: - version "2.10.0" - resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-2.10.0.tgz#9b57e9acc18eb44ebe9be5dc993753fd7d6d8e56" - integrity sha512-WCCaKuuzjZJl/xTaJiK2KB2lhHqAz+cTAHgSiZQc/pNnF2XUSZX0FBfxAG0qa9CogToNoQw7pEBJExc77QnFBQ== + version "2.11.0" + resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-2.11.0.tgz#18ff1b95a836b9b4e06854f796db2dc8fa98ce41" + integrity sha512-iIBCSis5yqtFYWtJAmNOiwDveFWWIn+8uV5UYuPHYu/Aeu5CSSJepSbaHMyfc+pPFgnsCcGzfPQEo7LSGmWbTg== cross-env@^7.0.0: version "7.0.3" @@ -10822,9 +10831,9 @@ cypress-plugin-snapshots@^1.4.4: unidiff "1.0.2" cypress@^10.0.0: - version "10.2.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-10.2.0.tgz#ca078abfceb13be2a33cbba6e0e80ded770f542a" - integrity sha512-+i9lY5ENlfi2mJwsggzR+XASOIgMd7S/Gd3/13NCpv596n3YSplMAueBTIxNLcxDpTcIksp+9pM3UaDrJDpFqA== + version "10.3.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae" + integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -11089,10 +11098,10 @@ dagre@^0.8.5: graphlib "^2.1.8" lodash "^4.17.15" -damerau-levenshtein@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" - integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== dargs@^7.0.0: version "7.0.0" @@ -12048,75 +12057,75 @@ es6-error@^4.1.1: resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild-android-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz#ef95b42c67bcf4268c869153fa3ad1466c4cea6b" - integrity sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g== +esbuild-android-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.48.tgz#7e6394a0e517f738641385aaf553c7e4fb6d1ae3" + integrity sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg== -esbuild-android-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz#4ebd7ce9fb250b4695faa3ee46fd3b0754ecd9e6" - integrity sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ== +esbuild-android-arm64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.48.tgz#6877566be0f82dd5a43030c0007d06ece7f7c02f" + integrity sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g== -esbuild-darwin-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz#e0da6c244f497192f951807f003f6a423ed23188" - integrity sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA== +esbuild-darwin-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.48.tgz#ea3caddb707d88f844b1aa1dea5ff3b0a71ef1fd" + integrity sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA== -esbuild-darwin-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz#cd40fd49a672fca581ed202834239dfe540a9028" - integrity sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw== +esbuild-darwin-arm64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.48.tgz#4e5eaab54df66cc319b76a2ac0e8af4e6f0d9c2f" + integrity sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA== -esbuild-freebsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz#8da6a14c095b29c01fc8087a16cb7906debc2d67" - integrity sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ== +esbuild-freebsd-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.48.tgz#47b5abc7426eae66861490ffbb380acc67af5b15" + integrity sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA== -esbuild-freebsd-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz#ad31f9c92817ff8f33fd253af7ab5122dc1b83f6" - integrity sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ== +esbuild-freebsd-arm64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.48.tgz#e8c54c8637cd44feed967ea12338b0a4da3a7b11" + integrity sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw== -esbuild-linux-32@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz#de085e4db2e692ea30c71208ccc23fdcf5196c58" - integrity sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw== +esbuild-linux-32@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.48.tgz#229cf3246de2b7937c3ac13fac622d4d7a1344c5" + integrity sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ== -esbuild-linux-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz#2a9321bbccb01f01b04cebfcfccbabeba3658ba1" - integrity sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw== +esbuild-linux-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.48.tgz#7c0e7226c02c42aacc5656c36977493dc1e96c4f" + integrity sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug== -esbuild-linux-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz#b9da7b6fc4b0ca7a13363a0c5b7bb927e4bc535a" - integrity sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw== +esbuild-linux-arm64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.48.tgz#0af1eda474b5c6cc0cace8235b74d0cb8fcf57a7" + integrity sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw== -esbuild-linux-arm@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz#56fec2a09b9561c337059d4af53625142aded853" - integrity sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA== +esbuild-linux-arm@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.48.tgz#de4d1fa6b77cdcd00e2bb43dd0801e4680f0ab52" + integrity sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw== -esbuild-linux-mips64le@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz#9db21561f8f22ed79ef2aedb7bbef082b46cf823" - integrity sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg== +esbuild-linux-mips64le@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.48.tgz#822c1778495f7868e990d4da47ad7281df28fd15" + integrity sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA== -esbuild-linux-ppc64le@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz#dc3a3da321222b11e96e50efafec9d2de408198b" - integrity sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w== +esbuild-linux-ppc64le@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.48.tgz#55de0a9ec4a48fedfe82a63e083164d001709447" + integrity sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ== -esbuild-linux-riscv64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz#9bd6dcd3dca6c0357084ecd06e1d2d4bf105335f" - integrity sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g== +esbuild-linux-riscv64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.48.tgz#cd2b7381880b2f4b21a5a598fb673492120f18a5" + integrity sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA== -esbuild-linux-s390x@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz#a458af939b52f2cd32fc561410d441a51f69d41f" - integrity sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw== +esbuild-linux-s390x@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.48.tgz#4b319eca2a5c64637fc7397ffbd9671719cdb6bf" + integrity sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g== esbuild-loader@^2.18.0: version "2.19.0" @@ -12130,61 +12139,61 @@ esbuild-loader@^2.18.0: tapable "^2.2.0" webpack-sources "^2.2.0" -esbuild-netbsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz#6388e785d7e7e4420cb01348d7483ab511b16aa8" - integrity sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ== +esbuild-netbsd-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.48.tgz#c27cde8b5cb55dcc227943a18ab078fb98d0adbf" + integrity sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw== -esbuild-openbsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5" - integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw== +esbuild-openbsd-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.48.tgz#af5ab2d1cb41f09064bba9465fc8bf1309150df1" + integrity sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA== -esbuild-sunos-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz#3f19612dcdb89ba6c65283a7ff6e16f8afbf8aaa" - integrity sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ== +esbuild-sunos-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.48.tgz#db3ae20526055cf6fd5c4582676233814603ac54" + integrity sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA== -esbuild-windows-32@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz#a92d279c8458d5dc319abcfeb30aa49e8f2e6f7f" - integrity sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ== +esbuild-windows-32@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.48.tgz#021ffceb0a3f83078262870da88a912293c57475" + integrity sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg== -esbuild-windows-64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz#2564c3fcf0c23d701edb71af8c52d3be4cec5f8a" - integrity sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ== +esbuild-windows-64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.48.tgz#a4d3407b580f9faac51f61eec095fa985fb3fee4" + integrity sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA== -esbuild-windows-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz#86d9db1a22d83360f726ac5fba41c2f625db6878" - integrity sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ== +esbuild-windows-arm64@0.14.48: + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.48.tgz#762c0562127d8b09bfb70a3c816460742dd82880" + integrity sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g== esbuild@^0.14.1, esbuild@^0.14.10, esbuild@^0.14.39: - version "0.14.47" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz#0d6415f6bd8eb9e73a58f7f9ae04c5276cda0e4d" - integrity sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA== + version "0.14.48" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.48.tgz#da5d8d25cd2d940c45ea0cfecdca727f7aee2b85" + integrity sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA== optionalDependencies: - esbuild-android-64 "0.14.47" - esbuild-android-arm64 "0.14.47" - esbuild-darwin-64 "0.14.47" - esbuild-darwin-arm64 "0.14.47" - esbuild-freebsd-64 "0.14.47" - esbuild-freebsd-arm64 "0.14.47" - esbuild-linux-32 "0.14.47" - esbuild-linux-64 "0.14.47" - esbuild-linux-arm "0.14.47" - esbuild-linux-arm64 "0.14.47" - esbuild-linux-mips64le "0.14.47" - esbuild-linux-ppc64le "0.14.47" - esbuild-linux-riscv64 "0.14.47" - esbuild-linux-s390x "0.14.47" - esbuild-netbsd-64 "0.14.47" - esbuild-openbsd-64 "0.14.47" - esbuild-sunos-64 "0.14.47" - esbuild-windows-32 "0.14.47" - esbuild-windows-64 "0.14.47" - esbuild-windows-arm64 "0.14.47" + esbuild-android-64 "0.14.48" + esbuild-android-arm64 "0.14.48" + esbuild-darwin-64 "0.14.48" + esbuild-darwin-arm64 "0.14.48" + esbuild-freebsd-64 "0.14.48" + esbuild-freebsd-arm64 "0.14.48" + esbuild-linux-32 "0.14.48" + esbuild-linux-64 "0.14.48" + esbuild-linux-arm "0.14.48" + esbuild-linux-arm64 "0.14.48" + esbuild-linux-mips64le "0.14.48" + esbuild-linux-ppc64le "0.14.48" + esbuild-linux-riscv64 "0.14.48" + esbuild-linux-s390x "0.14.48" + esbuild-netbsd-64 "0.14.48" + esbuild-openbsd-64 "0.14.48" + esbuild-sunos-64 "0.14.48" + esbuild-windows-32 "0.14.48" + esbuild-windows-64 "0.14.48" + esbuild-windows-arm64 "0.14.48" escalade@^3.1.1: version "3.1.1" @@ -12308,22 +12317,23 @@ eslint-plugin-jest@^26.1.2: "@typescript-eslint/utils" "^5.10.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.5.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" - integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== + version "6.6.0" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.0.tgz#2c5ac12e013eb98337b9aa261c3b355275cc6415" + integrity sha512-kTeLuIzpNhXL2CwLlc8AHI0aFRwWHcg483yepO9VQiHzM9bZwJdzTkzBszbuPrbgGmq2rlX/FaT2fJQsjUSHsw== dependencies: - "@babel/runtime" "^7.16.3" + "@babel/runtime" "^7.18.3" aria-query "^4.2.2" - array-includes "^3.1.4" + array-includes "^3.1.5" ast-types-flow "^0.0.7" - axe-core "^4.3.5" + axe-core "^4.4.2" axobject-query "^2.2.0" - damerau-levenshtein "^1.0.7" + damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" has "^1.0.3" - jsx-ast-utils "^3.2.1" + jsx-ast-utils "^3.3.1" language-tags "^1.0.5" - minimatch "^3.0.4" + minimatch "^3.1.2" + semver "^6.3.0" eslint-plugin-monorepo@^0.3.2: version "0.3.2" @@ -12353,9 +12363,9 @@ eslint-plugin-react-hooks@^4.3.0: integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.28.0: - version "7.30.0" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.0.tgz#8e7b1b2934b8426ac067a0febade1b13bd7064e3" - integrity sha512-RgwH7hjW48BleKsYyHK5vUAvxtE9SMPDKmcPRQgtRCYaZA0XQPt5FSkrU3nhz5ifzMZcA8opwmRJ2cmOO8tr5A== + version "7.30.1" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz#2be4ab23ce09b5949c6631413ba64b2810fd3e22" + integrity sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg== dependencies: array-includes "^3.1.5" array.prototype.flatmap "^1.3.0" @@ -12406,15 +12416,15 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint-webpack-plugin@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz#83dad2395e5f572d6f4d919eedaa9cf902890fcb" - integrity sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg== + version "3.2.0" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" + integrity sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w== dependencies: - "@types/eslint" "^7.28.2" - jest-worker "^27.3.1" - micromatch "^4.0.4" + "@types/eslint" "^7.29.0 || ^8.4.1" + jest-worker "^28.0.2" + micromatch "^4.0.5" normalize-path "^3.0.0" - schema-utils "^3.1.1" + schema-utils "^4.0.0" eslint@^8.6.0: version "8.18.0" @@ -14075,7 +14085,7 @@ got@11.8.3: p-cancelable "^2.0.0" responselike "^2.0.0" -got@^11.8.0: +got@^11.8.0, got@^11.8.2: version "11.8.5" resolved "https://registry.npmjs.org/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== @@ -16342,7 +16352,7 @@ jest-when@^3.1.0: resolved "https://registry.npmjs.org/jest-when/-/jest-when-3.5.1.tgz#33ab6f923661cf878cd08fe9df64b507934603db" integrity sha512-o+HiaIVCg1IC95sMDKHU9G5v5N5l3UHqXvJpf0PgAMThZeQo4Hf5Sgoj+wpCBRGg4/KtzSAZZZEKNiLqE0i4eQ== -jest-worker@^27.3.1, jest-worker@^27.4.5, jest-worker@^27.5.1: +jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -16351,6 +16361,15 @@ jest-worker@^27.3.1, jest-worker@^27.4.5, jest-worker@^27.5.1: merge-stream "^2.0.0" supports-color "^8.0.0" +jest-worker@^28.0.2: + version "28.1.1" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.1.tgz#3480c73247171dfd01eda77200f0063ab6a3bf28" + integrity sha512-Au7slXB08C6h+xbJPp7VIb6U0XX5Kc9uel/WFc6/rcTzGiaVCBRngBExSYuXSLFPULPSYU3cJ3ybS988lNFQhQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jest@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" @@ -16411,9 +16430,9 @@ jose@^4.1.4: integrity sha512-GFcVFQwYQKbQTUOo2JlpFGXTkgBw26uzDsRMD2q1WgSKNSnpKS9Ug7bdQ8dS+p4sZHNH6iRPu6WK2jLIjspaMA== jose@^4.6.0: - version "4.8.1" - resolved "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz#dc7c2660b115ba29b44880e588c5ac313c158247" - integrity sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw== + version "4.8.3" + resolved "https://registry.npmjs.org/jose/-/jose-4.8.3.tgz#5a754fb4aa5f2806608d083f438e6916b11087da" + integrity sha512-7rySkpW78d8LBp4YU70Wb7+OTgE3OwAALNVZxhoIhp4Kscp+p/fBkdpxGAMKxvCAMV4QfXBU9m6l9nX/vGwd2g== joycon@^3.0.1: version "3.1.0" @@ -16885,12 +16904,12 @@ jss@~10.8.2: array-includes "^3.1.2" object.assign "^4.1.2" -jsx-ast-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" - integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== +jsx-ast-utils@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.1.tgz#a3e0f1cb7e230954eab4dcbce9f6288a78f8ba44" + integrity sha512-pxrjmNpeRw5wwVeWyEAk7QJu2GnBO3uzPFmHCKJJFPKK2Cy0cWL23krGtLdnMmbIi6/FjlrQpPyfQI19ByPOhQ== dependencies: - array-includes "^3.1.3" + array-includes "^3.1.5" object.assign "^4.1.2" just-diff-apply@^4.0.1: @@ -16947,10 +16966,10 @@ jwt-decode@*, jwt-decode@^3.1.0: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -kafkajs@^1.16.0-beta.6: - version "1.16.0" - resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-1.16.0.tgz#bfcc3ae2b69265ca8435b53a01ee9e8787b9fee5" - integrity sha512-+Rcfu2hyQ/jv5skqRY8xA7Ra+mmRkDAzCaLDYbkGtgsNKpzxPWiLbk8ub0dgr4EbWrN1Zb4BCXHUkD6+zYfdWg== +kafkajs@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.0.2.tgz#cdfc8f57aa4fd69f6d9ca1cce4ee89bbc2a3a1f9" + integrity sha512-g6CM3fAenofOjR1bfOAqeZUEaSGhNtBscNokybSdW1rmIKYNwBPC9xQzwulFJm36u/xcxXUiCl/L/qfslapihA== keyv-memcache@^1.2.5: version "1.2.9" @@ -17009,10 +17028,10 @@ kleur@^4.0.3, kleur@^4.1.4: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== -knex@^1.0.2: - version "1.0.7" - resolved "https://registry.npmjs.org/knex/-/knex-1.0.7.tgz#965f4490efc451b140aac4c5c6efa39fd877597b" - integrity sha512-89jxuRATt4qJMb9ZyyaKBy0pQ4d5h7eOFRqiNFnUvsgU+9WZ2eIaZKrAPG1+F3mgu5UloPUnkVE5Yo2sKZUs6Q== +knex@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/knex/-/knex-2.1.0.tgz#9348aace3a08ff5be26eb1c8e838416ddf1aa216" + integrity sha512-vVsnD6UJdSJy55TvCXfFF9syfwyXNxfE9mvr2hJL/4Obciy2EPGoqjDpgRSlMruHuPWDOeYAG25nyrGvU+jJog== dependencies: colorette "2.0.16" commander "^9.1.0" @@ -17051,13 +17070,20 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -latest-version@5.1.0, latest-version@^5.1.0: +latest-version@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== dependencies: package-json "^6.3.0" +latest-version@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/latest-version/-/latest-version-6.0.0.tgz#469fb95f1d588b38436c1ec8fec947b99ed268c7" + integrity sha512-zfTuGx4PwpoSJ1mABs58AkM6qMzu49LZ7LT5JHprKvpGpQ+cYtfSibi3tLLrH4z7UylYU42rfBdwN8YgqbTljA== + dependencies: + package-json "^7.0.0" + lazy-ass@1.6.0, lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -17214,9 +17240,9 @@ linkify-it@^3.0.1: uc.micro "^1.0.1" lint-staged@^13.0.0: - version "13.0.2" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.2.tgz#35a1c57130e9ad5b1dea784972a40777ba433dd5" - integrity sha512-qQLfLTh9z34eMzfEHENC+QBskZfxjomrf+snF3xJ4BzilORbD989NLqQ00ughsF/A+PT41e87+WsMFabf9++pQ== + version "13.0.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz#d7cdf03a3830b327a2b63c6aec953d71d9dc48c6" + integrity sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug== dependencies: cli-truncate "^3.1.0" colorette "^2.0.17" @@ -18681,13 +18707,6 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== - dependencies: - brace-expansion "^1.1.7" - minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -19355,9 +19374,9 @@ npm-packlist@^3.0.0: npm-normalize-package-bin "^1.0.1" npm-packlist@^5.0.0, npm-packlist@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.0.tgz#f3fd52903a021009913a133732022132eb355ce7" - integrity sha512-a04sqF6FbkyOAFA19AA0e94gS7Et5T2/IMj3VOT9nOF2RaRdVPQ1Q17Fb/HaDRFs+gbC7HOmhVZ29adpWgmDZg== + version "5.1.1" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz#79bcaf22a26b6c30aa4dd66b976d69cc286800e0" + integrity sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw== dependencies: glob "^8.0.1" ignore-walk "^5.0.1" @@ -19636,9 +19655,9 @@ octokit-plugin-create-pull-request@^3.10.0: "@octokit/types" "^6.8.2" octokit@^1.7.1: - version "1.8.0" - resolved "https://registry.npmjs.org/octokit/-/octokit-1.8.0.tgz#c2ea6ace083b3be1d594aa7eb2e05fccd14e7ca5" - integrity sha512-HtArk9ttGy5effKNiaqKCirR6VSZoYjqgLgvVm/1mSRR4WYmww5DHjLnZlCbvj8+DwBLLLduJ3XnX/SPCCcp6A== + version "1.8.1" + resolved "https://registry.npmjs.org/octokit/-/octokit-1.8.1.tgz#399b0032e89e058084a1a1922e40a02e87d4cd61" + integrity sha512-xBLKFIivbl7wnLwxzLYuDO/JDNYxdyxoSjFrl/QMrY/fwGGQYYklvKUDTUyGMU0aXPrQtJ0IZnG3BXpCkDQzWg== dependencies: "@octokit/app" "^12.0.4" "@octokit/core" "^3.5.1" @@ -19749,9 +19768,9 @@ openid-client@^4.1.1: oidc-token-hash "^5.0.1" openid-client@^5.1.3: - version "5.1.6" - resolved "https://registry.npmjs.org/openid-client/-/openid-client-5.1.6.tgz#e5eb2032ecfdcfc108660b5c525910a14e352f11" - integrity sha512-HTFaXWdUHvLFw4GaEMgC0jXYBgpjgzQQNHW1pZsSqJorSgrXzxJ+4u/LWCGaClDEse5HLjXRV+zU5Bn3OefiZw== + version "5.1.7" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-5.1.7.tgz#deb16847c610075716be1ec679068b04db16f065" + integrity sha512-VNtf/q+fv2Jiqi0ViLVmN3gGMSHF+YUGW6baKA/naoPKkKw4JdvghaP/kXQ/bzRRDWk6VmpCYDcR934UdDs8ug== dependencies: jose "^4.1.4" lru-cache "^6.0.0" @@ -20012,6 +20031,16 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" +package-json@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/package-json/-/package-json-7.0.0.tgz#1355416e50a5c1b8f1a6f471197a3650d21186bf" + integrity sha512-CHJqc94AA8YfSLHGQT3DbvSIuE12NLFekpM4n7LRrAd3dOJtA911+4xe9q6nC3/jcKraq7nNS9VxgtT0KC+diA== + dependencies: + got "^11.8.2" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^7.3.5" + packet-reader@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" @@ -25678,11 +25707,6 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" -valid-url@^1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" - integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= - validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -25931,9 +25955,9 @@ webpack-dev-middleware@^5.3.1: schema-utils "^4.0.0" webpack-dev-server@^4.7.3: - version "4.9.2" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz#c188db28c7bff12f87deda2a5595679ebbc3c9bc" - integrity sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q== + version "4.9.3" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz#2360a5d6d532acb5410a668417ad549ee3b8a3c9" + integrity sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" @@ -25947,7 +25971,7 @@ webpack-dev-server@^4.7.3: chokidar "^3.5.3" colorette "^2.0.10" compression "^1.7.4" - connect-history-api-fallback "^1.6.0" + connect-history-api-fallback "^2.0.0" default-gateway "^6.0.3" express "^4.17.3" graceful-fs "^4.2.6"