Merge branch 'master' of https://github.com/backstage/backstage into add-additional-scaffolder-permissions
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': minor
|
||||
---
|
||||
|
||||
Adds 2 new commands `repo schema openapi diff` and `package schema openapi diff`. `repo schema openapi diff` is intended to power a new breaking changes check on pull requests and the package level command allows plugin developers to quickly see new API breaking changes. They're intended to be used in complement with the existing `repo schema openapi verify` command to validate your OpenAPI spec against a variety of things.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-email': patch
|
||||
---
|
||||
|
||||
Support relative links in notifications sent via email
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added config prop `ensureSchemaExists` to support postgres instances where user can create schemas but not databases.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
The `versions:bump` command will no longer exit with a non-zero status if the version bump fails due to forbidden duplicate package installations. It will now also provide more information about how to troubleshoot such an error. The set of forbidden duplicates has also been expanded to include all `@backstage/*-app-api` packages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Avoiding pre-loading display total count undefined for table counts
|
||||
@@ -0,0 +1,111 @@
|
||||
name: API Breaking Changes (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- 'API Breaking Changes (Trigger)'
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Add values from previous step
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
# "If you specify the access for any of these scopes, all of those that are not specified are set to none."
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
actions: read # Access cache
|
||||
outputs:
|
||||
git-ref: ${{ steps.event.outputs.GIT_REF }}
|
||||
pr-number: ${{ steps.event.outputs.PR_NUMBER }}
|
||||
action: ${{ steps.event.outputs.ACTION }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
disable-sudo: true
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
|
||||
- name: 'Download artifacts'
|
||||
# Fetch output (zip archive) from the workflow run that triggered this workflow.
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "preview-spec"
|
||||
})[0];
|
||||
if (matchArtifact === undefined) {
|
||||
throw TypeError('Build Artifact not found!');
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: 'Accept event from first stage'
|
||||
run: unzip preview-spec.zip event.json
|
||||
|
||||
- name: Read Event into ENV
|
||||
id: event
|
||||
run: |
|
||||
echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_OUTPUT
|
||||
echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT
|
||||
echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: DEBUG - Print Job Outputs
|
||||
if: ${{ runner.debug }}
|
||||
run: |
|
||||
echo "PR number: ${{ steps.event.outputs.PR_NUMBER }}"
|
||||
echo "Git Ref: ${{ steps.event.outputs.GIT_REF }}"
|
||||
echo "Action: ${{ steps.event.outputs.ACTION }}"
|
||||
cat event.json
|
||||
|
||||
- name: Get Comment
|
||||
id: get-comment
|
||||
run: |
|
||||
unzip preview-spec.zip comment.md
|
||||
ls
|
||||
grep
|
||||
|
||||
add-comment:
|
||||
name: Write comment about issues
|
||||
needs:
|
||||
- setup
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
# Identify comment to be updated
|
||||
- name: Find comment for API Changes
|
||||
uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ needs.setup.outputs.pr-number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: API changes
|
||||
direction: last
|
||||
|
||||
- name: Create or Update Comment with API Changes
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: comment.md
|
||||
edit-mode: replace
|
||||
@@ -0,0 +1,56 @@
|
||||
name: API Breaking Changes (Trigger)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
paths:
|
||||
- '**/openapi.yaml'
|
||||
|
||||
jobs:
|
||||
get-backstage-changes:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
name: Build PR image
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
# Fetch the commit that's merged into the base rather than the target ref
|
||||
# This will let us diff only the contents of the PR, without fetching more history
|
||||
ref: 'refs/pull/${{ github.event.pull_request.number }}/merge'
|
||||
- name: fetch base
|
||||
run: git fetch --depth 1 origin ${{ github.base_ref }}
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@a674369920067381b450d398b27df7039b7ef635 # v0.6.5
|
||||
with:
|
||||
cache-prefix: linux-v18
|
||||
|
||||
- name: breaking changes check
|
||||
run: |
|
||||
yarn backstage-repo-tools repo schema openapi check --since origin/${{ github.base_ref }} > comment.md
|
||||
|
||||
- name: Upload Rendered Comment as Artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: comment.md
|
||||
retention-days: 2
|
||||
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
# Fetch the commit that's merged into the base rather than the target ref
|
||||
# This will let us diff only the contents of the PR, without fetching more history
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.merge_commit_sha }}'
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- name: fetch master branch
|
||||
run: git fetch origin master
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
path: backstage
|
||||
ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -66,6 +66,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
# Fetch changes to previous commit - required for 'only_changed' in Prettier action
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: npm install semver@7.3.5 fs-extra@10.0.0 @manypkg/get-packages@1.1.1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
path: backstage
|
||||
# 'v' prefix is added here for the tag, we keep it out of the manifest logic
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
# Checkout backstage/versions into /backstage/versions, which is where store the output
|
||||
- name: Checkout versions
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
repository: backstage/versions
|
||||
path: backstage/versions
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- name: Monitor and Synchronize Snyk Policies
|
||||
uses: snyk/actions/node@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # master
|
||||
with:
|
||||
@@ -58,6 +58,6 @@ jobs:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=7168
|
||||
- name: Upload Snyk report
|
||||
uses: github/codeql-action/upload-sarif@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
fetch-depth: 20000
|
||||
fetch-tags: true
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- name: Render Compose File
|
||||
run: |
|
||||
# update image after the build above
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -80,4 +80,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
# Vale does not support file excludes, so we use the script to generate a list of files instead
|
||||
# The action also does not allow args or a local config file to be passed in, so the files array
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: Install Fossa
|
||||
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
fetch-depth: 0 # Required to retrieve git history
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
@@ -5,3 +5,4 @@ Portions of this software were developed by third-party software vendors:
|
||||
|
||||
- Tech Radar Plugin (https://opensource.zalando.com/tech-radar/), Copyright (c) 2017 Zalando SE
|
||||
- [OpenAPI Generator Templates](./packages/repo-tools/templates), Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) Copyright 2018 SmartBear Software
|
||||
- Optic CLI (https://github.com/opticdev/optic), Copyright 2022, Optic Labs Corporation
|
||||
|
||||
@@ -9,6 +9,9 @@ app:
|
||||
# applicationId: qwerty
|
||||
# site: # datadoghq.eu default = datadoghq.com
|
||||
# env: # optional
|
||||
# sessionSampleRate: 100
|
||||
# sessionReplaySampleRate: 0
|
||||
|
||||
support:
|
||||
url: https://github.com/backstage/backstage/issues # Used by common ErrorPage
|
||||
items: # Used by common SupportButton component
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 72 KiB |
@@ -22,6 +22,8 @@ app:
|
||||
applicationId: qwerty
|
||||
# site: datadoghq.eu
|
||||
# env: 'staging'
|
||||
# sessionSampleRate: 100
|
||||
# sessionReplaySampleRate: 0
|
||||
```
|
||||
|
||||
If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually.
|
||||
|
||||
@@ -20,7 +20,7 @@ info:
|
||||
|
||||
### Generating your client
|
||||
|
||||
1. Run `yarn backstage-repo-tools schema openapi generate client --output-package <directory>`. This will create a new folder in `<directory>/src/generated` to house the generated content.
|
||||
1. Run `yarn backstage-repo-tools package schema openapi generate client --client-package <directory>`. This will create a new folder in `<directory>/src/generated` to house the generated content.
|
||||
2. You should use the generated files as follows,
|
||||
|
||||
- `apis/DefaultApi.client.ts` - this is the client that you should use. It has types for all of the various operations on your API.
|
||||
|
||||
@@ -81,19 +81,21 @@ In order for Backstage to function properly the following versioning rules must
|
||||
be followed. The rules are referring to the
|
||||
[Package Architecture](https://backstage.io/docs/overview/architecture-overview#package-architecture).
|
||||
|
||||
- The versions of all the packages in the `Frontend App Core` must be from the
|
||||
same release, and it is recommended to keep `Common Tooling` on that release
|
||||
too.
|
||||
- The Backstage dependencies of any given plugin should be from the same
|
||||
release. This includes the packages from `Common Libraries`,
|
||||
`Frontend Plugin Core`, and `Frontend Libraries`, or alternatively the
|
||||
`Backend Libraries`.
|
||||
- There must be no package that is from a newer release than the
|
||||
`Frontend App Core` packages in the app.
|
||||
- The versions of all packages for each of the "App Core" groups must be from the
|
||||
same Backstage release.
|
||||
- For each frontend and backend setup, the "App Core" packages must be ahead of or on the same Backstage release as the "Plugin Core" packages, including transitive dependencies of all installed plugins and modules.
|
||||
- For any given plugin, the versions of all packages from the "Plugin Core" and
|
||||
"Library" groups must be from the same Backstage release.
|
||||
- Frontend plugins with a corresponding backend plugin should be from the same
|
||||
release. The update to the backend plugin **MUST** be deployed before or
|
||||
together with the update to the frontend plugin.
|
||||
|
||||
It is allowed and often expected that the "Plugin Core" and "Library" packages
|
||||
are from older releases than the "App Core" packages. It is also allowed to have
|
||||
duplicate installations of the "Plugin Core" and "Library" packages. This is all
|
||||
to make sure that upgrading Backstage is as smooth as possible and allows for
|
||||
more flexibility across the entire plugin ecosystem.
|
||||
|
||||
## Package Versioning Policy
|
||||
|
||||
Every individual package is versioned according to [semver](https://semver.org).
|
||||
|
||||
@@ -72,7 +72,10 @@
|
||||
site: '<%= config.getOptionalString("app.datadogRum.site") || "datadoghq.com" %>',
|
||||
service: 'backstage',
|
||||
env: '<%= config.getString("app.datadogRum.env") %>',
|
||||
sampleRate: 100,
|
||||
sampleRate:
|
||||
'<%= config.getOptionalNumber("app.datadogRum.sessionSampleRate") || 100 %>',
|
||||
sessionReplaySampleRate:
|
||||
'<%= config.getOptionalNumber("app.datadogRum.sessionReplaySampleRate") || 0 %>',
|
||||
trackInteractions: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,10 @@
|
||||
site: '<%= config.getOptionalString("app.datadogRum.site") || "datadoghq.com" %>',
|
||||
service: 'backstage',
|
||||
env: '<%= config.getString("app.datadogRum.env") %>',
|
||||
sampleRate: 100,
|
||||
sampleRate:
|
||||
'<%= config.getOptionalNumber("app.datadogRum.sessionSampleRate") || 100 %>',
|
||||
sessionReplaySampleRate:
|
||||
'<%= config.getOptionalNumber("app.datadogRum.sessionReplaySampleRate") || 0 %>',
|
||||
trackInteractions: true,
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+14
@@ -111,6 +111,13 @@ export interface Config {
|
||||
* Defaults to true if unspecified.
|
||||
*/
|
||||
ensureExists?: boolean;
|
||||
/**
|
||||
* Whether to ensure the given database schema exists by creating it if it does not.
|
||||
* Defaults to false if unspecified.
|
||||
*
|
||||
* NOTE: Currently only supported by the `pg` client when pluginDivisionMode: schema
|
||||
*/
|
||||
ensureSchemaExists?: boolean;
|
||||
/**
|
||||
* How plugins databases are managed/divided in the provided database instance.
|
||||
*
|
||||
@@ -147,6 +154,13 @@ export interface Config {
|
||||
* Defaults to base config if unspecified.
|
||||
*/
|
||||
ensureExists?: boolean;
|
||||
/**
|
||||
* Whether to ensure the given database schema exists by creating it if it does not.
|
||||
* Defaults to false if unspecified.
|
||||
*
|
||||
* NOTE: Currently only supported by the `pg` client when pluginDivisionMode: schema
|
||||
*/
|
||||
ensureSchemaExists?: boolean;
|
||||
/**
|
||||
* Arbitrary config object to pass to knex when initializing
|
||||
* (https://knexjs.org/#Installation-client). Most notable is the
|
||||
|
||||
@@ -321,7 +321,10 @@ export class PgConnector implements Connector {
|
||||
let schemaOverrides;
|
||||
if (this.getPluginDivisionModeConfig() === 'schema') {
|
||||
schemaOverrides = this.getSchemaOverrides(pluginId);
|
||||
if (this.getEnsureExistsConfig(pluginId)) {
|
||||
if (
|
||||
this.getEnsureSchemaExistsConfig(pluginId) ||
|
||||
this.getEnsureExistsConfig(pluginId)
|
||||
) {
|
||||
try {
|
||||
await pgConnector.ensureSchemaExists!(pluginConfig, pluginId);
|
||||
} catch (error) {
|
||||
@@ -437,6 +440,16 @@ export class PgConnector implements Connector {
|
||||
);
|
||||
}
|
||||
|
||||
private getEnsureSchemaExistsConfig(pluginId: string): boolean {
|
||||
const baseConfig =
|
||||
this.config.getOptionalBoolean('ensureSchemaExists') ?? false;
|
||||
return (
|
||||
this.config.getOptionalBoolean(
|
||||
`${pluginPath(pluginId)}.getEnsureSchemaExistsConfig`,
|
||||
) ?? baseConfig
|
||||
);
|
||||
}
|
||||
|
||||
private getPluginDivisionModeConfig(): string {
|
||||
return this.config.getOptionalString('pluginDivisionMode') ?? 'database';
|
||||
}
|
||||
|
||||
+36
-26
@@ -1,34 +1,46 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"version": "0.26.5-next.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/cli"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"start": "nodemon --"
|
||||
},
|
||||
"bin": {
|
||||
"backstage-cli": "bin/backstage-cli"
|
||||
},
|
||||
"files": [
|
||||
"asset-types",
|
||||
"templates",
|
||||
"config",
|
||||
"bin",
|
||||
"dist/**/*.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"start": "nodemon --",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "bin/backstage-cli",
|
||||
"ext": "ts",
|
||||
"watch": "./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
@@ -197,18 +209,6 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"asset-types",
|
||||
"templates",
|
||||
"config",
|
||||
"bin",
|
||||
"dist/**/*.js"
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"watch": "./src",
|
||||
"exec": "bin/backstage-cli",
|
||||
"ext": "ts"
|
||||
},
|
||||
"configSchema": {
|
||||
"$schema": "https://backstage.io/schema/config-v1",
|
||||
"title": "@backstage/cli",
|
||||
@@ -248,6 +248,16 @@
|
||||
"type": "string",
|
||||
"visibility": "frontend",
|
||||
"description": "site for Datadog RUM events"
|
||||
},
|
||||
"sessionSampleRate": {
|
||||
"type": "number",
|
||||
"visibility": "frontend",
|
||||
"description": "sample rate of Datadog RUM events"
|
||||
},
|
||||
"sessionReplaySampleRate": {
|
||||
"type": "number",
|
||||
"visibility": "frontend",
|
||||
"description": "sample rate of session replays based upon already sampled Datadog RUM events"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -894,30 +894,19 @@ describe('bump', () => {
|
||||
newVersions: [],
|
||||
newRanges: [
|
||||
{
|
||||
name: 'first-duplicate',
|
||||
oldRange: 'first-duplicate',
|
||||
newRange: 'first-duplicate',
|
||||
oldVersion: '1.0.0',
|
||||
newVersion: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: 'second-duplicate',
|
||||
oldRange: 'second-duplicate',
|
||||
newRange: 'second-duplicate',
|
||||
oldVersion: '1.0.0',
|
||||
newVersion: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: 'third-duplicate',
|
||||
oldRange: 'third-duplicate',
|
||||
newRange: 'third-duplicate',
|
||||
name: '@backstage/backend-app-api',
|
||||
oldRange: '^1.0.0',
|
||||
newRange: '^2.0.0',
|
||||
oldVersion: '1.0.0',
|
||||
newVersion: '2.0.0',
|
||||
},
|
||||
],
|
||||
});
|
||||
mockDir.setContent({
|
||||
'yarn.lock': lockfileMock,
|
||||
'yarn.lock': `${HEADER}
|
||||
"@backstage/backend-app-api@^1.0.0":
|
||||
version "1.0.0"
|
||||
`,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
@@ -928,16 +917,7 @@ describe('bump', () => {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
'@backstage/backend-app-api': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -952,7 +932,12 @@ describe('bump', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [],
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/backend-app-api',
|
||||
version: '2.0.0',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -962,24 +947,20 @@ describe('bump', () => {
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Checking for updates of @backstage/core-api',
|
||||
'Checking for updates of @backstage/backend-app-api',
|
||||
'Checking for updates of @backstage/backend-app-api',
|
||||
'Some packages are outdated, updating',
|
||||
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
|
||||
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
|
||||
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
|
||||
'bumping @backstage/core in a to ^1.0.6',
|
||||
'bumping @backstage/core in b to ^1.0.6',
|
||||
'bumping @backstage/theme in b to ^2.0.0',
|
||||
'bumping @backstage/backend-app-api in a to ^2.0.0',
|
||||
'Running yarn install to install new versions',
|
||||
'Checking for moved packages to the @backstage-community namespace...',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/theme : 1.0.0 ~> 2.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
' @backstage/backend-app-api : 1.0.0 ~> 2.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/backend-app-api/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
'The following packages have duplicates but have been allowed:',
|
||||
'first-duplicate, second-duplicate, third-duplicate',
|
||||
' ⚠️ Warning! ⚠️',
|
||||
' The below package(s) have incompatible duplicate installations, likely due to a bad dependency in a plugin.',
|
||||
' You can investigate this by running `yarn why <package-name>`, and report the issue to the plugin maintainers.',
|
||||
' @backstage/backend-app-api',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,24 +331,22 @@ export default async (opts: OptionValues) => {
|
||||
forbiddenDuplicatesFilter(name),
|
||||
);
|
||||
if (forbiddenNewRanges.length > 0) {
|
||||
throw new Error(
|
||||
`Version bump failed for ${forbiddenNewRanges
|
||||
.map(i => i.name)
|
||||
.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowedDuplicates = result.newRanges.filter(
|
||||
({ name }) => !forbiddenDuplicatesFilter(name),
|
||||
);
|
||||
|
||||
if (allowedDuplicates.length > 0) {
|
||||
console.log(chalk.yellow(' ⚠️ Warning! ⚠️'));
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'The following packages have duplicates but have been allowed:',
|
||||
' The below package(s) have incompatible duplicate installations, likely due to a bad dependency in a plugin.',
|
||||
),
|
||||
);
|
||||
console.log(chalk.yellow(allowedDuplicates.map(i => i.name).join(', ')));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' You can investigate this by running `yarn why <package-name>`, and report the issue to the plugin maintainers.',
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
for (const { name } of forbiddenNewRanges) {
|
||||
console.log(chalk.yellow(` ${name}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ export const includedFilter = (name: string) =>
|
||||
INCLUDED.some(pattern => pattern.test(name));
|
||||
|
||||
// Packages that are not allowed to have any duplicates
|
||||
const FORBID_DUPLICATES = [
|
||||
/^@backstage\/core-app-api$/,
|
||||
/^@backstage\/plugin-/,
|
||||
];
|
||||
const FORBID_DUPLICATES = [/^@backstage\/\w+-app-api$/, /^@backstage\/plugin-/];
|
||||
|
||||
// There are some packages that ARE explicitly allowed to have duplicates since
|
||||
// they handle that appropriately. This takes precedence over FORBID_DUPLICATES
|
||||
|
||||
@@ -172,9 +172,20 @@ Commands:
|
||||
lint [options] [paths...]
|
||||
test [options] [paths...]
|
||||
fuzz [options]
|
||||
diff [options]
|
||||
help [command]
|
||||
```
|
||||
|
||||
### `backstage-repo-tools repo schema openapi diff`
|
||||
|
||||
```
|
||||
Usage: backstage-repo-tools repo schema openapi diff [options]
|
||||
|
||||
Options:
|
||||
--since <ref>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-repo-tools repo schema openapi fuzz`
|
||||
|
||||
```
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@stoplight/spectral-rulesets": "^1.18.0",
|
||||
"@stoplight/spectral-runtime": "^1.1.2",
|
||||
"@stoplight/types": "^14.0.0",
|
||||
"@useoptic/openapi-utilities": "^0.54.8",
|
||||
"chalk": "^4.0.0",
|
||||
"codeowners-utils": "^1.0.2",
|
||||
"command-exists": "^1.2.9",
|
||||
|
||||
@@ -78,6 +78,15 @@ function registerPackageCommand(program: Command) {
|
||||
.action(
|
||||
lazy(() => import('./package/schema/openapi/fuzz').then(m => m.command)),
|
||||
);
|
||||
|
||||
openApiCommand
|
||||
.command('diff')
|
||||
.option('--ignore', 'Ignore linting failures and only log the results.')
|
||||
.option('--json', 'Output the results as JSON')
|
||||
.option('--since <ref>', 'Diff the API against a specific ref')
|
||||
.action(
|
||||
lazy(() => import('./package/schema/openapi/diff').then(m => m.command)),
|
||||
);
|
||||
}
|
||||
|
||||
function registerRepoCommand(program: Command) {
|
||||
@@ -96,7 +105,7 @@ function registerRepoCommand(program: Command) {
|
||||
openApiCommand
|
||||
.command('verify [paths...]')
|
||||
.description(
|
||||
'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.',
|
||||
'Verify that all OpenAPI schemas are valid and set up correctly.',
|
||||
)
|
||||
.action(
|
||||
lazy(() =>
|
||||
@@ -133,6 +142,20 @@ function registerRepoCommand(program: Command) {
|
||||
.action(
|
||||
lazy(() => import('./repo/schema/openapi/fuzz').then(m => m.command)),
|
||||
);
|
||||
|
||||
openApiCommand
|
||||
.command('diff')
|
||||
.description(
|
||||
'Diff the repository against a specific ref, will run all package `diff` scripts.',
|
||||
)
|
||||
.option(
|
||||
'--since <ref>',
|
||||
'Diff the API against a specific ref',
|
||||
'origin/master',
|
||||
)
|
||||
.action(
|
||||
lazy(() => import('./repo/schema/openapi/diff').then(m => m.command)),
|
||||
);
|
||||
}
|
||||
|
||||
export function registerCommands(program: Command) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import chalk from 'chalk';
|
||||
import { exec } from '../../../../lib/exec';
|
||||
import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers';
|
||||
import { paths as cliPaths } from '../../../../lib/paths';
|
||||
import { OptionValues } from 'commander';
|
||||
import { env } from 'process';
|
||||
import { readFile, rm } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const reduceOpticOutput = (output: string) => {
|
||||
return output
|
||||
.split('\n')
|
||||
.filter(e => !e.startsWith('Rerun') && e.trim())
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
async function check(opts: OptionValues) {
|
||||
const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec();
|
||||
|
||||
let baseRef = opts.since;
|
||||
if (!baseRef) {
|
||||
const { stdout: branch } = await exec(
|
||||
'git merge-base --fork-point origin/master',
|
||||
);
|
||||
baseRef = branch.toString().trim();
|
||||
}
|
||||
|
||||
let failed = false;
|
||||
let output = '';
|
||||
try {
|
||||
const { stdout } = await exec(
|
||||
'yarn optic diff',
|
||||
[
|
||||
resolvedOpenapiPath,
|
||||
'--check',
|
||||
opts.json ? '--json' : '',
|
||||
'--base',
|
||||
baseRef,
|
||||
],
|
||||
{
|
||||
cwd: cliPaths.targetRoot,
|
||||
env: { CI: opts.json ? '1' : undefined, ...env },
|
||||
},
|
||||
);
|
||||
output = stdout.toString();
|
||||
} catch (err) {
|
||||
output = err.stdout;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
const file = (
|
||||
await readFile(resolve(cliPaths.targetRoot, 'ci-run-details.json'))
|
||||
).toString();
|
||||
const results = JSON.parse(file);
|
||||
console.log(file);
|
||||
if (!opts.ignore && results.failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
|
||||
await rm(resolve(cliPaths.targetRoot, 'ci-run-details.json'));
|
||||
} else {
|
||||
console.log(reduceOpticOutput(output));
|
||||
if (!opts.ignore && failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
try {
|
||||
await check(opts);
|
||||
if (!opts.json) console.log(chalk.green(`All checks passed.`));
|
||||
} catch (err) {
|
||||
if (!opts.json) console.log(chalk.red(err.message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { OptionValues } from 'commander';
|
||||
import { exec } from '../../../../lib/exec';
|
||||
import {
|
||||
CiRunDetails,
|
||||
generateCompareSummaryMarkdown,
|
||||
} from '../../../../lib/openapi/optic/helpers';
|
||||
import { paths as cliPaths } from '../../../../lib/paths';
|
||||
import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants';
|
||||
|
||||
function cleanUpApiName(e: { apiName: string }) {
|
||||
e.apiName = e.apiName
|
||||
.replace(cliPaths.targetDir, '')
|
||||
.replace(YAML_SCHEMA_PATH, '');
|
||||
}
|
||||
|
||||
export async function command(opts: OptionValues) {
|
||||
let packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
let since = '';
|
||||
if (opts.since) {
|
||||
const { stdout: sinceRaw } = await exec('git', ['rev-parse', opts.since]);
|
||||
since = sinceRaw.toString().trim();
|
||||
const { stdout: changedFilesRaw } = await exec('git', [
|
||||
'diff',
|
||||
'--name-only',
|
||||
since,
|
||||
]);
|
||||
const changedFiles = changedFilesRaw.toString().trim();
|
||||
|
||||
const changedOpenApiSpecs = changedFiles
|
||||
.split('\n')
|
||||
.filter(e => e.endsWith(YAML_SCHEMA_PATH))
|
||||
.map(e => cliPaths.resolveTarget(e));
|
||||
|
||||
// filter packages by changedFiles
|
||||
packages = packages.filter(pkg =>
|
||||
changedOpenApiSpecs.some(e => e.startsWith(`${pkg.dir}/`)),
|
||||
);
|
||||
}
|
||||
|
||||
const checkablePackages = packages.filter(e => e.packageJson.scripts?.diff);
|
||||
|
||||
try {
|
||||
const outputs = {
|
||||
completed: [],
|
||||
failed: [],
|
||||
noop: [],
|
||||
warning: [],
|
||||
severity: 0,
|
||||
} as CiRunDetails;
|
||||
for (const pkg of checkablePackages) {
|
||||
const sinceCommands = since ? ['--since', since] : [];
|
||||
const { stdout } = await exec(
|
||||
'yarn',
|
||||
['diff', '--ignore', '--json', ...sinceCommands],
|
||||
{
|
||||
cwd: pkg.dir,
|
||||
},
|
||||
);
|
||||
const result = JSON.parse(stdout.toString());
|
||||
outputs.completed.push(...(result.completed ?? []));
|
||||
outputs.failed.push(...(result.failed ?? []));
|
||||
outputs.noop.push(...(result.noop ?? []));
|
||||
}
|
||||
|
||||
for (const pkg of packages.filter(e => !e.packageJson.scripts?.diff)) {
|
||||
outputs.warning?.push({
|
||||
apiName: `${pkg.dir}/`,
|
||||
warning: 'No diff script found in package.json',
|
||||
});
|
||||
}
|
||||
|
||||
outputs.completed.forEach(cleanUpApiName);
|
||||
outputs.failed.forEach(cleanUpApiName);
|
||||
outputs.noop.forEach(cleanUpApiName);
|
||||
outputs.warning?.forEach(cleanUpApiName);
|
||||
|
||||
const { stdout: currentSha } = await exec('git', ['rev-parse', 'HEAD']);
|
||||
console.log(
|
||||
generateCompareSummaryMarkdown(
|
||||
{ sha: currentSha.toString().trim() },
|
||||
outputs,
|
||||
{ verbose: true },
|
||||
),
|
||||
);
|
||||
|
||||
const failed = outputs.failed.length > 0;
|
||||
if (failed) {
|
||||
throw new Error('Some checks failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ async function verify(directoryPath: string) {
|
||||
// Unable to find spec at path.
|
||||
return;
|
||||
}
|
||||
|
||||
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
|
||||
await Parser.validate(cloneDeep(yaml) as any);
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
import {
|
||||
compareSpecs,
|
||||
groupDiffsByEndpoint,
|
||||
Severity,
|
||||
getOperationsChangedLabel,
|
||||
getOperationsChanged,
|
||||
} from '@useoptic/openapi-utilities';
|
||||
import { GroupedDiffs } from '@useoptic/openapi-utilities/build/openapi3/group-diff';
|
||||
|
||||
/**
|
||||
* The below code is copied from https://github.com/opticdev/optic/blob/main/projects/optic/src/commands/ci/comment/common.ts#L82 for use
|
||||
* with a security flow for forked repositories.
|
||||
*/
|
||||
|
||||
type Comparison = {
|
||||
groupedDiffs: ReturnType<typeof groupDiffsByEndpoint>;
|
||||
results: Awaited<ReturnType<typeof compareSpecs>>['results'];
|
||||
};
|
||||
|
||||
export type CiRunDetails = {
|
||||
completed: {
|
||||
warnings: string[];
|
||||
apiName: string;
|
||||
opticWebUrl?: string | null;
|
||||
comparison: Comparison;
|
||||
specUrl?: string | null;
|
||||
capture?: any;
|
||||
}[];
|
||||
warning?: { apiName: string; warning: string }[];
|
||||
failed: { apiName: string; error: string }[];
|
||||
noop: { apiName: string }[];
|
||||
severity: Severity;
|
||||
};
|
||||
|
||||
const getChecksLabel = (
|
||||
results: CiRunDetails['completed'][number]['comparison']['results'],
|
||||
severity: Severity,
|
||||
) => {
|
||||
const totalChecks = results.length;
|
||||
let failingChecks = 0;
|
||||
let exemptedFailingChecks = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result.passed) continue;
|
||||
if (result.severity < severity) continue;
|
||||
if (result.exempted) exemptedFailingChecks += 1;
|
||||
else failingChecks += 1;
|
||||
}
|
||||
|
||||
const exemptedChunk =
|
||||
exemptedFailingChecks > 0 ? `, ${exemptedFailingChecks} exempted` : '';
|
||||
|
||||
return failingChecks > 0
|
||||
? `⚠️ **${failingChecks}**/**${totalChecks}** failed${exemptedChunk}`
|
||||
: totalChecks > 0
|
||||
? `✅ **${totalChecks}** passed${exemptedChunk}`
|
||||
: `ℹ️ No automated checks have run`;
|
||||
};
|
||||
|
||||
function getOperationsText(
|
||||
groupedDiffs: GroupedDiffs,
|
||||
options: { webUrl?: string | null; verbose: boolean; labelJoiner?: string },
|
||||
) {
|
||||
const ops = getOperationsChanged(groupedDiffs);
|
||||
|
||||
const operationsText = options.verbose
|
||||
? [
|
||||
...[...ops.added].map(o => `\`${o}\` (added)`),
|
||||
...[...ops.changed].map(o => `\`${o}\` (changed)`),
|
||||
...[...ops.removed].map(o => `\`${o}\` (removed)`),
|
||||
].join('\n')
|
||||
: '';
|
||||
return `${getOperationsChangedLabel(groupedDiffs)}
|
||||
|
||||
${operationsText}
|
||||
`;
|
||||
}
|
||||
|
||||
const getCaptureIssuesLabel = ({
|
||||
unmatchedInteractions,
|
||||
mismatchedEndpoints,
|
||||
}: {
|
||||
unmatchedInteractions: number;
|
||||
mismatchedEndpoints: number;
|
||||
}) => {
|
||||
return [
|
||||
...(unmatchedInteractions
|
||||
? [
|
||||
`🆕 ${unmatchedInteractions} undocumented path${
|
||||
unmatchedInteractions > 1 ? 's' : ''
|
||||
}`,
|
||||
]
|
||||
: []),
|
||||
...(mismatchedEndpoints
|
||||
? [
|
||||
`⚠️ ${mismatchedEndpoints} mismatch${
|
||||
mismatchedEndpoints > 1 ? 'es' : ''
|
||||
}`,
|
||||
]
|
||||
: []),
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
const getBreakagesRow = (breakage: CiRunDetails['completed'][number]) => {
|
||||
return `
|
||||
- ${breakage.apiName}
|
||||
${breakage.comparison.results.map(
|
||||
s => `
|
||||
- ${s.where}
|
||||
${'```'}
|
||||
${s.error}
|
||||
${'```'}`,
|
||||
)}`;
|
||||
};
|
||||
|
||||
const addSummaryLine = (items: any[] | number | undefined, label: string) => {
|
||||
const length = Array.isArray(items) ? items.length : items;
|
||||
if (!length) return '';
|
||||
let text = length === 1 ? `1 API` : `${length} APIs`;
|
||||
text += ` had ${label}`;
|
||||
return text;
|
||||
};
|
||||
|
||||
export const generateCompareSummaryMarkdown = (
|
||||
commit: { sha: string },
|
||||
results: CiRunDetails,
|
||||
options: { verbose: boolean },
|
||||
) => {
|
||||
const anyCompletedHasWarning = results.completed.some(
|
||||
s => s.warnings.length > 0,
|
||||
);
|
||||
const anyCompletedHasCapture = results.completed.some(s => s.capture);
|
||||
if (
|
||||
results.completed.length === 0 &&
|
||||
results.failed.length === 0 &&
|
||||
results.failed.length === 0
|
||||
) {
|
||||
return `No API changes detected for commit (${commit.sha})`;
|
||||
}
|
||||
const breakages = results.completed
|
||||
.filter(s => s.comparison.results.some(e => !e.passed))
|
||||
.map(e => ({
|
||||
...e,
|
||||
comparison: {
|
||||
...e.comparison,
|
||||
results: e.comparison.results.filter(f => !f.passed),
|
||||
},
|
||||
}));
|
||||
const successfullyCompletedCount =
|
||||
results.completed.length - breakages.length;
|
||||
return `### Summary for commit (${commit.sha})
|
||||
|
||||
${addSummaryLine(results.noop, 'no changes')}
|
||||
|
||||
${addSummaryLine(breakages.length, 'breaking changes')}
|
||||
|
||||
${addSummaryLine(successfullyCompletedCount, 'non-breaking changes')}
|
||||
|
||||
${addSummaryLine(results.warning, 'warnings')}
|
||||
|
||||
${
|
||||
results.completed.length > 0
|
||||
? `### APIs with Changes
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<th>Changes</th>
|
||||
<th>Rules</th>
|
||||
${anyCompletedHasWarning ? '<th>Warnings</th>' : ''}
|
||||
${anyCompletedHasCapture ? '<th>Tests</th>' : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.completed
|
||||
.map(
|
||||
s =>
|
||||
`<tr>
|
||||
<td>
|
||||
${s.apiName}
|
||||
</td>
|
||||
<td>
|
||||
${getOperationsText(s.comparison.groupedDiffs, {
|
||||
webUrl: s.opticWebUrl,
|
||||
verbose: options.verbose,
|
||||
labelJoiner: ',\n',
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
${getChecksLabel(s.comparison.results, results.severity)}
|
||||
</td>
|
||||
|
||||
${anyCompletedHasWarning ? `<td>${s.warnings.join('\n')}</td>` : ''}
|
||||
|
||||
${
|
||||
anyCompletedHasCapture
|
||||
? `
|
||||
<td>
|
||||
${
|
||||
s.capture
|
||||
? s.capture.success
|
||||
? s.capture.mismatchedEndpoints || s.capture.unmatchedInteractions
|
||||
? getCaptureIssuesLabel({
|
||||
unmatchedInteractions: s.capture.unmatchedInteractions,
|
||||
mismatchedEndpoints: s.capture.mismatchedEndpoints,
|
||||
})
|
||||
: `✅ ${s.capture.percentCovered}% coverage`
|
||||
: '❌ Failed to run'
|
||||
: ''
|
||||
}
|
||||
</td>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</tr>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ''
|
||||
}
|
||||
|
||||
${
|
||||
results.failed.length > 0
|
||||
? `### APIs with Errors
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.failed
|
||||
.map(
|
||||
s => `<tr>
|
||||
<td>${s.apiName}</td>
|
||||
<td>
|
||||
|
||||
${'```'}
|
||||
${s.error}
|
||||
${'```'}
|
||||
|
||||
</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
${
|
||||
results.warning && results.warning.length
|
||||
? `
|
||||
### APIs with Warnings
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<th>Warning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.warning
|
||||
.map(e => `<tr><td>${e.apiName}</td><td>${e.warning}</td></tr>`)
|
||||
.join('\n')}
|
||||
</tbody>
|
||||
</table>`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
breakages.length > 0
|
||||
? `
|
||||
### Routes with Breakages
|
||||
|
||||
${breakages.map(getBreakagesRow).join('\n')}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
};
|
||||
@@ -43,6 +43,7 @@
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"diff": "backstage-repo-tools package schema openapi diff",
|
||||
"fuzz": "backstage-repo-tools package schema openapi fuzz --exclude-checks response_schema_conformance",
|
||||
"generate": "backstage-repo-tools package schema openapi generate --server --client-package packages/catalog-client",
|
||||
"lint": "backstage-cli package lint",
|
||||
|
||||
@@ -343,7 +343,7 @@ describe('CatalogTable component', () => {
|
||||
expect(screen.getByText('Should be rendered')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the label column with customised title and value as specified', async () => {
|
||||
it('should render the label column with customized title and value as specified', async () => {
|
||||
const columns = [
|
||||
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
|
||||
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
|
||||
@@ -381,7 +381,7 @@ describe('CatalogTable component', () => {
|
||||
expect(labelCellValue).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the label column with customised title and value as specified using function', async () => {
|
||||
it('should render the label column with customized title and value as specified using function', async () => {
|
||||
const columns: CatalogTableColumnsFunc = ({
|
||||
filters,
|
||||
entities: entities1,
|
||||
|
||||
@@ -91,7 +91,6 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
const { loading, error, entities, filters, pageInfo, totalItems } =
|
||||
entityListContext;
|
||||
const enablePagination = !!pageInfo;
|
||||
|
||||
const tableColumns = useMemo(
|
||||
() =>
|
||||
typeof columns === 'function' ? columns(entityListContext) : columns,
|
||||
@@ -170,13 +169,18 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
|
||||
const currentKind = filters.kind?.value || '';
|
||||
const currentType = filters.type?.value || '';
|
||||
const currentCount = typeof totalItems === 'number' ? `(${totalItems})` : '';
|
||||
// TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar
|
||||
const titlePreamble = capitalize(filters.user?.value ?? 'all');
|
||||
const titleDisplay = [titlePreamble, currentType, pluralize(currentKind)]
|
||||
const title = [
|
||||
titlePreamble,
|
||||
currentType,
|
||||
pluralize(currentKind),
|
||||
currentCount,
|
||||
]
|
||||
.filter(s => s)
|
||||
.join(' ');
|
||||
|
||||
const title = `${titleDisplay} (${totalItems})`;
|
||||
const actions = props.actions || defaultActions;
|
||||
const options = {
|
||||
actionsColumnIndex: -1,
|
||||
@@ -192,7 +196,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
columns={tableColumns}
|
||||
emptyContent={emptyContent}
|
||||
isLoading={loading}
|
||||
title={titleDisplay}
|
||||
title={title}
|
||||
actions={actions}
|
||||
subtitle={subtitle}
|
||||
options={options}
|
||||
|
||||
+177
-74
@@ -30,6 +30,36 @@ jest.mock('nodemailer', () => ({
|
||||
createTransport: jest.fn(),
|
||||
}));
|
||||
|
||||
const DEFAULT_ENTITIES_RESPONSE = {
|
||||
items: [
|
||||
{
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
email: 'mock@backstage.io',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_SENDMAIL_CONFIG = {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transportConfig: {
|
||||
transport: 'sendmail',
|
||||
path: '/usr/local/bin/sendmail',
|
||||
},
|
||||
sender: 'backstage@backstage.io',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('NotificationsEmailProcessor', () => {
|
||||
const logger = mockServices.logger.mock();
|
||||
const auth = mockServices.auth();
|
||||
@@ -49,6 +79,10 @@ describe('NotificationsEmailProcessor', () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
externalBaseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
@@ -95,6 +129,10 @@ describe('NotificationsEmailProcessor', () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
externalBaseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
@@ -135,6 +173,10 @@ describe('NotificationsEmailProcessor', () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
externalBaseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
@@ -175,29 +217,10 @@ describe('NotificationsEmailProcessor', () => {
|
||||
|
||||
it('should send user email', async () => {
|
||||
(createTransport as jest.Mock).mockReturnValue(mockTransport);
|
||||
getEntityRefMock.mockResolvedValue({
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
email: 'mock@backstage.io',
|
||||
},
|
||||
},
|
||||
});
|
||||
getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]);
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transportConfig: {
|
||||
transport: 'sendmail',
|
||||
path: '/usr/local/bin/sendmail',
|
||||
},
|
||||
sender: 'backstage@backstage.io',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
mockServices.rootConfig({ data: DEFAULT_SENDMAIL_CONFIG }),
|
||||
mockCatalogClient as unknown as CatalogClient,
|
||||
auth,
|
||||
);
|
||||
@@ -218,41 +241,29 @@ describe('NotificationsEmailProcessor', () => {
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p></p>',
|
||||
html: '<p><a href="https://example.org/notifications">https://example.org/notifications</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: '',
|
||||
text: 'https://example.org/notifications',
|
||||
to: 'mock@backstage.io',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email to all', async () => {
|
||||
(createTransport as jest.Mock).mockReturnValue(mockTransport);
|
||||
getEntitiesMock.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
email: 'mock@backstage.io',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
getEntitiesMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE);
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transportConfig: {
|
||||
transport: 'sendmail',
|
||||
path: '/usr/local/bin/sendmail',
|
||||
},
|
||||
sender: 'backstage@backstage.io',
|
||||
broadcastConfig: {
|
||||
receiver: 'users',
|
||||
mockServices.rootConfig({
|
||||
data: {
|
||||
...DEFAULT_SENDMAIL_CONFIG,
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
...DEFAULT_SENDMAIL_CONFIG.notifications.processors.email,
|
||||
broadcastConfig: {
|
||||
receiver: 'users',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -278,42 +289,30 @@ describe('NotificationsEmailProcessor', () => {
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p></p>',
|
||||
html: '<p><a href="https://example.org/notifications">https://example.org/notifications</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: '',
|
||||
text: 'https://example.org/notifications',
|
||||
to: 'mock@backstage.io',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email to configured addresses', async () => {
|
||||
(createTransport as jest.Mock).mockReturnValue(mockTransport);
|
||||
getEntitiesMock.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
kind: 'User',
|
||||
spec: {
|
||||
profile: {
|
||||
email: 'mock@backstage.io',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
getEntitiesMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE);
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transportConfig: {
|
||||
transport: 'sendmail',
|
||||
path: '/usr/local/bin/sendmail',
|
||||
},
|
||||
sender: 'backstage@backstage.io',
|
||||
broadcastConfig: {
|
||||
receiver: 'config',
|
||||
receiverEmails: ['broadcast@backstage.io'] as JsonArray,
|
||||
mockServices.rootConfig({
|
||||
data: {
|
||||
...DEFAULT_SENDMAIL_CONFIG,
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
...DEFAULT_SENDMAIL_CONFIG.notifications.processors.email,
|
||||
broadcastConfig: {
|
||||
receiver: 'config',
|
||||
receiverEmails: ['broadcast@backstage.io'] as JsonArray,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -339,11 +338,115 @@ describe('NotificationsEmailProcessor', () => {
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p></p>',
|
||||
html: '<p><a href="https://example.org/notifications">https://example.org/notifications</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: '',
|
||||
text: 'https://example.org/notifications',
|
||||
to: 'broadcast@backstage.io',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email with relative link to given address', async () => {
|
||||
(createTransport as jest.Mock).mockReturnValue(mockTransport);
|
||||
getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]);
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
mockServices.rootConfig({
|
||||
data: DEFAULT_SENDMAIL_CONFIG,
|
||||
}),
|
||||
mockCatalogClient as unknown as CatalogClient,
|
||||
auth,
|
||||
);
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: 'catalog/user/default/john.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p><a href="https://example.org/catalog/user/default/john.doe">https://example.org/catalog/user/default/john.doe</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: 'https://example.org/catalog/user/default/john.doe',
|
||||
to: 'mock@backstage.io',
|
||||
});
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: '/catalog/user/default/jane.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p><a href="https://example.org/catalog/user/default/jane.doe">https://example.org/catalog/user/default/jane.doe</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: 'https://example.org/catalog/user/default/jane.doe',
|
||||
to: 'mock@backstage.io',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email with absolute link to given address', async () => {
|
||||
(createTransport as jest.Mock).mockReturnValue(mockTransport);
|
||||
getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]);
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
mockServices.rootConfig({
|
||||
data: DEFAULT_SENDMAIL_CONFIG,
|
||||
}),
|
||||
mockCatalogClient as unknown as CatalogClient,
|
||||
auth,
|
||||
);
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: 'https://backstage.io',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(sendmailMock).toHaveBeenCalledWith({
|
||||
from: 'backstage@backstage.io',
|
||||
html: '<p><a href="https://backstage.io/">https://backstage.io/</a></p>',
|
||||
replyTo: undefined,
|
||||
subject: 'notification',
|
||||
text: 'https://backstage.io/',
|
||||
to: 'mock@backstage.io',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+38
-6
@@ -50,6 +50,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor {
|
||||
private readonly cacheTtl: number;
|
||||
private readonly concurrencyLimit: number;
|
||||
private readonly throttleInterval: number;
|
||||
private readonly frontendBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
@@ -78,6 +79,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor {
|
||||
this.cacheTtl = cacheConfig
|
||||
? durationToMilliseconds(readDurationFromConfig(cacheConfig))
|
||||
: 3_600_000;
|
||||
this.frontendBaseUrl = config.getString('app.baseUrl');
|
||||
}
|
||||
|
||||
private async getTransporter() {
|
||||
@@ -215,20 +217,50 @@ export class NotificationsEmailProcessor implements NotificationProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
private async sendPlainEmail(notification: Notification, emails: string[]) {
|
||||
private getNotificationLink(notification: Notification) {
|
||||
if (notification.payload.link) {
|
||||
const stripLeadingSlash = (s: string) => s.replace(/^\//, '');
|
||||
const ensureTrailingSlash = (s: string) => s.replace(/\/?$/, '/');
|
||||
|
||||
try {
|
||||
const url = new URL(
|
||||
stripLeadingSlash(notification.payload.link),
|
||||
ensureTrailingSlash(this.frontendBaseUrl),
|
||||
);
|
||||
return url.toString();
|
||||
} catch (_e) {
|
||||
// noop: fallback to relative URL
|
||||
}
|
||||
return notification.payload.link;
|
||||
}
|
||||
return `${this.frontendBaseUrl}/notifications`;
|
||||
}
|
||||
|
||||
private getHtmlContent(notification: Notification) {
|
||||
const contentParts: string[] = [];
|
||||
if (notification.payload.description) {
|
||||
contentParts.push(`${notification.payload.description}`);
|
||||
}
|
||||
if (notification.payload.link) {
|
||||
contentParts.push(`${notification.payload.link}`);
|
||||
}
|
||||
const link = this.getNotificationLink(notification);
|
||||
contentParts.push(`<a href="${link}">${link}</a>`);
|
||||
return `<p>${contentParts.join('<br/>')}</p>`;
|
||||
}
|
||||
|
||||
private getTextContent(notification: Notification) {
|
||||
const contentParts: string[] = [];
|
||||
if (notification.payload.description) {
|
||||
contentParts.push(notification.payload.description);
|
||||
}
|
||||
contentParts.push(this.getNotificationLink(notification));
|
||||
return contentParts.join('\n\n');
|
||||
}
|
||||
|
||||
private async sendPlainEmail(notification: Notification, emails: string[]) {
|
||||
const mailOptions = {
|
||||
from: this.sender,
|
||||
subject: notification.payload.title,
|
||||
html: `<p>${contentParts.join('<br/>')}</p>`,
|
||||
text: contentParts.join('\n\n'),
|
||||
html: this.getHtmlContent(notification),
|
||||
text: this.getTextContent(notification),
|
||||
replyTo: this.replyTo,
|
||||
};
|
||||
|
||||
|
||||
@@ -7644,6 +7644,7 @@ __metadata:
|
||||
"@types/is-glob": ^4.0.2
|
||||
"@types/node": ^18.17.8
|
||||
"@types/prettier": ^2.0.0
|
||||
"@useoptic/openapi-utilities": ^0.54.8
|
||||
chalk: ^4.0.0
|
||||
codeowners-utils: ^1.0.2
|
||||
command-exists: ^1.2.9
|
||||
@@ -17345,6 +17346,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/json-pointer-helpers@npm:0.54.8":
|
||||
version: 0.54.8
|
||||
resolution: "@useoptic/json-pointer-helpers@npm:0.54.8"
|
||||
dependencies:
|
||||
jsonpointer: ^5.0.1
|
||||
minimatch: 9.0.3
|
||||
checksum: 4eddabb6dce3ca8160dcd4904299b6964945c3fe47d39bfeca6c68b9a50b058b901a6fb10ab168295475d651df3349149faa5f27f77293e15b6eee8d4417432e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/openapi-io@npm:0.50.10":
|
||||
version: 0.50.10
|
||||
resolution: "@useoptic/openapi-io@npm:0.50.10"
|
||||
@@ -17399,6 +17410,31 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/openapi-utilities@npm:^0.54.8":
|
||||
version: 0.54.8
|
||||
resolution: "@useoptic/openapi-utilities@npm:0.54.8"
|
||||
dependencies:
|
||||
"@useoptic/json-pointer-helpers": 0.54.8
|
||||
ajv: ^8.6.0
|
||||
ajv-errors: ~3.0.0
|
||||
ajv-formats: ~2.1.0
|
||||
chalk: ^4.1.2
|
||||
fast-deep-equal: ^3.1.3
|
||||
is-url: ^1.2.4
|
||||
js-yaml: ^4.1.0
|
||||
json-stable-stringify: ^1.0.1
|
||||
lodash.groupby: ^4.6.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.omit: ^4.5.0
|
||||
node-machine-id: ^1.1.12
|
||||
openapi-types: ^12.0.2
|
||||
ts-invariant: ^0.9.3
|
||||
url-join: ^4.0.1
|
||||
yaml-ast-parser: ^0.0.43
|
||||
checksum: fa9e9f430c77687591aaf8b43b7b31a7c2f80fe9c140aaa978f1948f84d3e974181c91c3d8ec3e06efca9735c7826290baf4be72063bf733887aa632b40c3c4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@useoptic/optic@npm:^0.50.10":
|
||||
version: 0.50.10
|
||||
resolution: "@useoptic/optic@npm:0.50.10"
|
||||
|
||||
Reference in New Issue
Block a user