Merge branch 'master' into plugin-azure-functions

Signed-off-by: Wesley Pattison <wesley.pattison@friss.com>
This commit is contained in:
Wesley Pattison
2022-09-26 12:07:14 +02:00
336 changed files with 9035 additions and 7795 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
consistently show parent and child relations in group profile card
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-model': patch
---
Defer `ajv` compilation of schema validators to improve module-import performance
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/create-app': patch
---
Newly created Backstage repositories now use the stable version 6 of
`react-router`, just like the main repo does. Please let us know if you find any
issues with this.
Migrating to the stable version of `react-router` is optional for the time
being. But if you want to do the same for your existing repository, please
follow [this
guide](https://backstage.io/docs/tutorials/react-router-stable-migration).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Update versions of packages used in the create-app template, to match those in the main repo
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-auth-backend': minor
---
CookieConfigurer can optionally return the `SameSite` cookie attribute.
CookieConfigurer now requires an additional argument `appOrigin` - the origin URL of the app - which is used to calculate the `SameSite` attribute.
defaultCookieConfigurer returns the `SameSite` attribute which defaults to `Lax`. In cases where an auth-backend is running on a different domain than the App, `SameSite=None` is used - but only for secure contexts. This is so that cookies can be included in third-party requests.
OAuthAdapterOptions has been modified to require additional arguments, `baseUrl`, and `cookieConfigurer`.
OAuthAdapter now resolves cookie configuration using its supplied CookieConfigurer for each request to make sure that the proper attributes always are set.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated fallback versions of dependencies in all templates.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
Add useCustomResources react hook for fetching Kubernetes Custom Resources
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Added a new `repo test` command.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/test-utils': patch
'@backstage/plugin-cicd-statistics': patch
---
Align on the version of `@material-ui/icons` used, to `^4.9.1` like other packages in the main repo
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated lockfile parsing to have better support for Yarn 3.
+179
View File
@@ -0,0 +1,179 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.75",
"@backstage/app-defaults": "1.0.6",
"example-backend": "0.2.75",
"@backstage/backend-app-api": "0.2.1",
"@backstage/backend-common": "0.15.1",
"@backstage/backend-defaults": "0.1.1",
"example-backend-next": "0.0.3",
"@backstage/backend-plugin-api": "0.1.2",
"@backstage/backend-tasks": "0.3.5",
"@backstage/backend-test-utils": "0.1.28",
"@backstage/catalog-client": "1.1.0",
"@backstage/catalog-model": "1.1.1",
"@backstage/cli": "0.19.0",
"@backstage/cli-common": "0.1.10",
"@backstage/codemods": "0.1.39",
"@backstage/config": "1.0.2",
"@backstage/config-loader": "1.1.4",
"@backstage/core-app-api": "1.1.0",
"@backstage/core-components": "0.11.1",
"@backstage/core-plugin-api": "1.0.6",
"@backstage/create-app": "0.4.31",
"@backstage/dev-utils": "1.0.6",
"e2e-test": "0.2.0",
"@backstage/errors": "1.1.1",
"@backstage/integration": "1.3.1",
"@backstage/integration-react": "1.1.4",
"@backstage/release-manifests": "0.0.6",
"@techdocs/cli": "1.2.1",
"techdocs-cli-embedded-app": "0.2.74",
"@backstage/test-utils": "1.2.0",
"@backstage/theme": "0.2.16",
"@backstage/types": "1.0.0",
"@backstage/version-bridge": "1.0.1",
"@backstage/plugin-adr": "0.2.1",
"@backstage/plugin-adr-backend": "0.2.1",
"@backstage/plugin-adr-common": "0.2.1",
"@backstage/plugin-airbrake": "0.3.9",
"@backstage/plugin-airbrake-backend": "0.2.9",
"@backstage/plugin-allure": "0.1.25",
"@backstage/plugin-analytics-module-ga": "0.1.20",
"@backstage/plugin-apache-airflow": "0.2.2",
"@backstage/plugin-api-docs": "0.8.9",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.0",
"@backstage/plugin-apollo-explorer": "0.1.2",
"@backstage/plugin-app-backend": "0.3.36",
"@backstage/plugin-auth-backend": "0.16.0",
"@backstage/plugin-auth-node": "0.2.5",
"@backstage/plugin-azure-devops": "0.2.0",
"@backstage/plugin-azure-devops-backend": "0.3.15",
"@backstage/plugin-azure-devops-common": "0.3.0",
"@backstage/plugin-badges": "0.2.33",
"@backstage/plugin-badges-backend": "0.1.30",
"@backstage/plugin-bazaar": "0.1.24",
"@backstage/plugin-bazaar-backend": "0.1.20",
"@backstage/plugin-bitbucket-cloud-common": "0.1.3",
"@backstage/plugin-bitrise": "0.1.36",
"@backstage/plugin-catalog": "1.5.1",
"@backstage/plugin-catalog-backend": "1.4.0",
"@backstage/plugin-catalog-backend-module-aws": "0.1.9",
"@backstage/plugin-catalog-backend-module-azure": "0.1.7",
"@backstage/plugin-catalog-backend-module-bitbucket": "0.2.3",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.3",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.1",
"@backstage/plugin-catalog-backend-module-gerrit": "0.1.4",
"@backstage/plugin-catalog-backend-module-github": "0.1.7",
"@backstage/plugin-catalog-backend-module-gitlab": "0.1.7",
"@backstage/plugin-catalog-backend-module-ldap": "0.5.3",
"@backstage/plugin-catalog-backend-module-msgraph": "0.4.2",
"@backstage/plugin-catalog-backend-module-openapi": "0.1.2",
"@backstage/plugin-catalog-common": "1.0.6",
"@internal/plugin-catalog-customized": "0.0.2",
"@backstage/plugin-catalog-graph": "0.2.21",
"@backstage/plugin-catalog-graphql": "0.3.13",
"@backstage/plugin-catalog-import": "0.8.12",
"@backstage/plugin-catalog-node": "1.1.0",
"@backstage/plugin-catalog-react": "1.1.4",
"@backstage/plugin-cicd-statistics": "0.1.11",
"@backstage/plugin-cicd-statistics-module-gitlab": "0.1.5",
"@backstage/plugin-circleci": "0.3.9",
"@backstage/plugin-cloudbuild": "0.3.9",
"@backstage/plugin-code-climate": "0.1.9",
"@backstage/plugin-code-coverage": "0.2.2",
"@backstage/plugin-code-coverage-backend": "0.2.2",
"@backstage/plugin-codescene": "0.1.4",
"@backstage/plugin-config-schema": "0.1.32",
"@backstage/plugin-cost-insights": "0.11.31",
"@backstage/plugin-cost-insights-common": "0.1.1",
"@backstage/plugin-dynatrace": "0.2.0",
"@internal/plugin-todo-list": "1.0.5",
"@internal/plugin-todo-list-backend": "1.0.5",
"@internal/plugin-todo-list-common": "1.0.4",
"@backstage/plugin-explore": "0.3.40",
"@backstage/plugin-explore-react": "0.0.21",
"@backstage/plugin-firehydrant": "0.1.26",
"@backstage/plugin-fossa": "0.2.41",
"@backstage/plugin-gcalendar": "0.3.5",
"@backstage/plugin-gcp-projects": "0.3.28",
"@backstage/plugin-git-release-manager": "0.3.22",
"@backstage/plugin-github-actions": "0.5.9",
"@backstage/plugin-github-deployments": "0.1.40",
"@backstage/plugin-github-issues": "0.1.1",
"@backstage/plugin-github-pull-requests-board": "0.1.3",
"@backstage/plugin-gitops-profiles": "0.3.27",
"@backstage/plugin-gocd": "0.1.15",
"@backstage/plugin-graphiql": "0.2.41",
"@backstage/plugin-graphql-backend": "0.1.26",
"@backstage/plugin-home": "0.4.25",
"@backstage/plugin-ilert": "0.1.35",
"@backstage/plugin-jenkins": "0.7.8",
"@backstage/plugin-jenkins-backend": "0.1.26",
"@backstage/plugin-jenkins-common": "0.1.8",
"@backstage/plugin-kafka": "0.3.9",
"@backstage/plugin-kafka-backend": "0.2.29",
"@backstage/plugin-kubernetes": "0.7.2",
"@backstage/plugin-kubernetes-backend": "0.7.2",
"@backstage/plugin-kubernetes-common": "0.4.2",
"@backstage/plugin-lighthouse": "0.3.9",
"@backstage/plugin-newrelic": "0.3.27",
"@backstage/plugin-newrelic-dashboard": "0.2.2",
"@backstage/plugin-org": "0.5.9",
"@backstage/plugin-pagerduty": "0.5.2",
"@backstage/plugin-periskop": "0.1.7",
"@backstage/plugin-periskop-backend": "0.1.7",
"@backstage/plugin-permission-backend": "0.5.11",
"@backstage/plugin-permission-common": "0.6.4",
"@backstage/plugin-permission-node": "0.6.5",
"@backstage/plugin-permission-react": "0.4.5",
"@backstage/plugin-playlist": "0.1.0",
"@backstage/plugin-playlist-backend": "0.1.0",
"@backstage/plugin-playlist-common": "0.1.0",
"@backstage/plugin-proxy-backend": "0.2.30",
"@backstage/plugin-rollbar": "0.4.9",
"@backstage/plugin-rollbar-backend": "0.1.33",
"@backstage/plugin-scaffolder": "1.6.0",
"@backstage/plugin-scaffolder-backend": "1.6.0",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.11",
"@backstage/plugin-scaffolder-backend-module-rails": "0.4.4",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.9",
"@backstage/plugin-scaffolder-common": "1.2.0",
"@backstage/plugin-search": "1.0.2",
"@backstage/plugin-search-backend": "1.0.2",
"@backstage/plugin-search-backend-module-elasticsearch": "1.0.2",
"@backstage/plugin-search-backend-module-pg": "0.4.0",
"@backstage/plugin-search-backend-node": "1.0.2",
"@backstage/plugin-search-common": "1.0.1",
"@backstage/plugin-search-react": "1.1.0",
"@backstage/plugin-sentry": "0.4.2",
"@backstage/plugin-shortcuts": "0.3.1",
"@backstage/plugin-sonarqube": "0.4.1",
"@backstage/plugin-sonarqube-backend": "0.1.1",
"@backstage/plugin-splunk-on-call": "0.3.33",
"@backstage/plugin-stack-overflow": "0.1.5",
"@backstage/plugin-stack-overflow-backend": "0.1.5",
"@backstage/plugin-tech-insights": "0.3.0",
"@backstage/plugin-tech-insights-backend": "0.5.2",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.20",
"@backstage/plugin-tech-insights-common": "0.2.6",
"@backstage/plugin-tech-insights-node": "0.3.4",
"@backstage/plugin-tech-radar": "0.5.16",
"@backstage/plugin-techdocs": "1.3.2",
"@backstage/plugin-techdocs-addons-test-utils": "1.0.4",
"@backstage/plugin-techdocs-backend": "1.3.0",
"@backstage/plugin-techdocs-module-addons-contrib": "1.0.4",
"@backstage/plugin-techdocs-node": "1.4.0",
"@backstage/plugin-techdocs-react": "1.0.4",
"@backstage/plugin-todo": "0.2.11",
"@backstage/plugin-todo-backend": "0.1.33",
"@backstage/plugin-user-settings": "0.4.8",
"@backstage/plugin-user-settings-backend": "0.1.0",
"@backstage/plugin-vault": "0.1.3",
"@backstage/plugin-vault-backend": "0.2.2",
"@backstage/plugin-xcmetrics": "0.2.29"
},
"changesets": []
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins-backend': patch
---
Fixed a bug where `extraRequestHeaders` configuration was ignored.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Updated dependency `@asyncapi/react-component` to `1.0.0-next.42`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Switched the Jest transform for YAML files to use a custom one available at `@backstage/cli/config/jestYamlTransform.js`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Allow Placeholder value to be any value, not only string.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': minor
---
EntityPickers now support flags to control when to include default namespace
in result
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-insights-node': patch
---
Updated package role to be `node-library`.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-user-settings': minor
---
**BREAKING**: The `apiRef` passed to `ProviderSettingsItem` now needs to
implement `ProfileInfoApi & SessionApi`, rather than just the latter. This is
unlikely to have an effect on most users though, since the builtin auth
providers generally implement both.
Fixed settings page showing providers as logged out when the user is using more
than one provider, and displayed some additional login information.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Increased test database max connection pool size to reduce the risk of resource exhaustion.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/cli': minor
---
**BREAKING**: Bumped `jest`, `jest-runtime`, and `jest-environment-jsdom` to v29. This is up from v27, so check out both the [v28](https://jestjs.io/docs/28.x/upgrading-to-jest28) and [v29](https://jestjs.io/docs/upgrading-to-jest29) (later [here](https://jestjs.io/docs/29.x/upgrading-to-jest29)) migration guides.
Particular changes that where encountered in the main Backstage repo are:
- The updated snapshot format.
- `jest.useFakeTimers('legacy')` is now `jest.useFakeTimers({ legacyFakeTimers: true })`.
- Error objects collected by `withLogCollector` from `@backstage/test-utils` are now objects with a `detail` property rather than a string.
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/cli': patch
---
Removed `tsx` and `jsx` as supported extensions in backend packages. For most
repos, this will not have any effect. But if you inadvertently had added some
`tsx`/`jsx` files to your backend package, you may now start to see `code: 'MODULE_NOT_FOUND'` errors when launching the backend locally. The reason for
this is that the offending files get ignored during transpilation. Hence, the
importing file can no longer find anything to import.
The fix is to rename any `.tsx` files in your backend packages to `.ts` instead,
or `.jsx` to `.js`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Add functionality to shutdown scaffolder tasks if they are stale
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Make sure to stitch entities correctly after deletion, to ensure that their relations are updated.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/create-app': patch
---
Updated the root `test` scripts to use `backstage-cli repo test`.
To apply this change to an existing app, make the following change to the root `package.json`:
```diff
- "test": "backstage-cli test",
- "test:all": "lerna run test -- --coverage",
+ "test": "backstage-cli repo test",
+ "test:all": "backstage-cli repo test --coverage",
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Fixed bug in `CostOverviewBreakdownChart` component where some datasets caused the cost overview breakdown chart to tear.
+2 -2
View File
@@ -197,7 +197,7 @@ jobs:
- name: test changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn lerna run test --since origin/master -- --coverage --runInBand
run: yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --since origin/master
env:
BACKSTAGE_NEXT_TESTS: 1
BACKSTAGE_TEST_DISABLE_DOCKER: 1
@@ -208,7 +208,7 @@ jobs:
- name: test all packages (and upload coverage)
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: |
yarn lerna run test -- --coverage --runInBand
yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --coverage
bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD)
env:
BACKSTAGE_NEXT_TESTS: 1
+3 -1
View File
@@ -55,7 +55,9 @@ jobs:
# not flagged as the latest release, which means that people will not get this
# version of the package unless requested explicitly
- name: publish nightly release
run: yarn changeset publish --tag nightly
run: |
yarn config set -H 'npmAuthToken' "${{secrets.NPM_TOKEN}}"
yarn workspaces foreach -v --no-private npm publish --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+4 -3
View File
@@ -97,7 +97,7 @@ jobs:
- name: test (and upload coverage)
run: |
yarn lerna run test -- --coverage --runInBand
yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --coverage
bash <(curl -s https://codecov.io/bash)
# Upload code coverage for some specific flags. Also see .codecov.yml
bash <(curl -s https://codecov.io/bash) -f packages/core-app-api/coverage/* -F core-app-api
@@ -162,10 +162,11 @@ jobs:
# Publishes current version of packages that are not already present in the registry
- name: publish
run: |
yarn config set -H 'npmAuthToken' "${{secrets.NPM_TOKEN}}"
if [ -f ".changeset/pre.json" ]; then
yarn lerna publish from-package --yes --dist-tag next
yarn workspaces foreach -v --no-private npm publish --access public --tag next
else
yarn lerna publish from-package --yes
yarn workspaces foreach -v --no-private npm publish --access public
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
run: yarn lint:type-deps
- name: test
run: yarn lerna run test
run: yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M
env:
BACKSTAGE_NEXT_TESTS: 1
BACKSTAGE_TEST_DISABLE_DOCKER: 1
@@ -0,0 +1,47 @@
# Enabling SSL for Local Testing
If you need to use an `https:` URL for local testing (i.e. if an OAuth provider requires a "secure" callback URL), you can use a self-signed certificate by following these steps.
## Backend
1. Generate a self-signed certificate and key for localhost and configure your system to trust it. The application [`mkcert`](https://github.com/FiloSottile/mkcert) is a helpful tool to accomplish this.
1. Update `backend.baseUrl` in app-config.local.yaml to use an `https:` address.
1. Add the certificate and key to `backend.https.certificate.cert` and `backend.https.certificate.cert`, respectively.
```yaml
backend:
baseUrl: https://localhost:7007
https:
certificate:
# You may copy the contents of the file...
cert: |
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUZ9VhZckcy690L
...
-----END CERTIFICATE-----
# ... or use a path
key:
$file: ./certs/localhost-key.pem
```
1. Start the backend with `NODE_EXTRA_CA_CERTS=/absolute/path/to/cert.pem yarn start-backend`
## Frontend
1. As with the backend instructions above, a trusted certificate and key are needed.
1. Update `app.baseUrl` and `backend.cors.origin` in app-config.local.yaml to use an `https:` address.
1. Add the certificate and key to `app.https.certificate.cert` and `app.https.certificate.cert`, respectively.
```yaml
app:
baseUrl: https://localhost:3000
https:
certificate:
cert:
$file: ./certs/localhost.pem
key:
$file: ./certs/localhost-key.pem
backend:
cors:
origin: https://localhost:3000
```
1. and start the app with `yarn start`.
Depending on what plugins are in use, you may need to override additional URLs to use `https` for those endpoints to work.
+1526 -1131
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
---
id: auth-backend
title: Auth backend
description: Documentation on Auth backend
---
## TODO
+31 -1
View File
@@ -18,7 +18,7 @@ The providers currently available are divided into server side and client side.
## Server Side Providers
These providers authenticate your _application_ with the cluster, meaning anyone that is
logged in into your backstage app will be granted the same access to Kubernetes objects.
logged in into your backstage app will be granted the same access to Kubernetes objects, including guest users.
The providers available as server side are:
@@ -28,6 +28,33 @@ The providers available as server side are:
- `localKubectlProxy`
- `serviceAccount`
### Azure
The Azure server side authentication provider works by authenticating on the server with
the Azure CLI, please note that [Azure AD Authentication][1] is a requirement and has to
be enabled in your AKS cluster, then follow these steps:
- [Install the Azure CLI][2] in the environment where the backstage application will run.
- Login with your Azure/Microsoft account with `az login` in the server's terminal.
- Go to your AKS cluster's resource page in Azure Console and follow the steps in the
`Connect` tab to set the subscription and get your credentials for `kubectl` integration.
- Configure your cluster to use the `azure` auth provider like this:
```yaml
kubernetes:
clusterLocatorMethods:
- type: 'config'
clusters:
- name: My AKS cluster
url: ${AZURE_CLUSTER_API_SERVER_ADDRESS}
authProvider: azure
skipTLSVerify: true
```
To get the API server address for your Azure cluster, go to the Azure console page for the
cluster resource, go to `Overview` > `Properties` tab > `Networking` section and copy paste
the API server address directly in that `url` field.
## Client Side Providers
These providers authenticate your _user_ with the cluster. Each Backstage user will be
@@ -41,3 +68,6 @@ The providers available as client side are:
- `google`
- `oidc`
[1]: https://docs.microsoft.com/en-us/azure/aks/managed-aad
[2]: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest
@@ -413,6 +413,7 @@ rules:
- ingresses
- statefulsets
- limitranges
- daemonsets
verbs:
- get
- list
@@ -156,16 +156,17 @@ export default async function createPlugin(
Here is a list of Open Source custom actions that you can add to your Backstage
scaffolder backend:
| Name | Package | Owner |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
| Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
| Name | Package | Owner |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
| Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
| Azure Repository Actions | [scaffolder-backend-module-azure-repositories](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-repositories) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
Have fun! 🚀
@@ -0,0 +1,89 @@
---
id: writing-custom-step-layouts
title: Writing custom step layouts
description: How to override the default step form layout
---
Every form in each step rendered in the frontend uses the default form layout from [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
This is the same [field](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/custom-templates/#objectfieldtemplate) used by [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component.
## Registering a React component as a custom step layout
The [createScaffolderLayout](https://backstage.io/docs/reference/plugin-scaffolder.createscaffolderlayout) function is used to mark a component as a custom step layout:
```ts
import React from 'react';
import {
createScaffolderLayout,
LayoutTemplate,
scaffolderPlugin,
} from '@backstage/plugin-scaffolder';
import { Grid } from '@material-ui/core';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
const mid = Math.ceil(properties.length / 2);
return (
<>
<h1>{title}</h1>
<h2>In two column layout!!</h2>
<Grid container justifyContent="flex-end">
{properties.slice(0, mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
{properties.slice(mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
</Grid>
{description}
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
```
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
```tsx
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
...
</FlatRoutes>
);
```
## Using the custom step layout
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
@@ -258,6 +258,10 @@ use `ui:widget: password` or set some properties of `ui:backstage`:
show: false # won't print any info about 'hidden' property on Review Step
```
### Custom step layouts
If you find that the default layout of the form used in a particular step does not meet your needs then you can supply your own [custom step layout](./writing-custom-step-layouts.md).
### Remove sections or fields based on feature flags
Based on feature flags you can hide sections or even only fields of your
+1 -1
View File
@@ -165,7 +165,7 @@ Open `packages/app/src/App.tsx` and below the last `import` line, add:
```typescript
import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { SignInProviderConfig, SignInPage } from '@backstage/core-components';
import { SignInPage } from '@backstage/core-components';
```
Search for `const app = createApp({` in this file, and below `apis,` add:
+38
View File
@@ -0,0 +1,38 @@
---
id: frontend-integration
title: Frontend Integration
description: How to place your Backstage frontend components behind authorization
---
Now that we understand how to [author a permission policy](./writing-a-policy.md), let's consider cases where we'll need to supplement our policy with authorization checks on the frontend.
In most cases, actual functionality that live within various plugins will already have been placed behind authorization by the plugin authors. The permission backend will use your permission policy to return an authorization result, and the plugin frontend will correspondingly show/hide/disable the relevant UI component.
However, there are some cases where the integrator needs to supplement the policy on the frontend. One example is app level routing.
If your Backstage permission policy may return a `DENY` for users requesting the `catalogEntityCreatePermission`, it may make sense, for example, to remove access to the `/catalog-import` page entirely:
```diff
// packages/app/src/App.tsx
...
+ import { RequirePermission } from '@backstage/plugin-permission-react';
+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
...
- <Route path="/catalog-import" element={<CatalogImportPage />} />
+ <Route
+ path="/catalog-import"
+ element={
+ <RequirePermission permission={catalogEntityCreatePermission}>
+ <CatalogImportPage />
+ </RequirePermission>
+ }
+ />
...
```
With this change, users who are denied the `catalogEntityCreatePermission` should now be unable to access the `/catalog-import` page.
@@ -20,15 +20,18 @@ Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add
- export const tempExamplePermission = createPermission({
- name: 'temp.example.noop',
- attributes: {},
+ export const todoListCreate = createPermission({
+ export const todoListCreatePermission = createPermission({
+ name: 'todo.list.create',
+ attributes: { action: 'create' },
});
- export const todoListPermissions = [tempExamplePermission];
+ export const todoListPermissions = [todoListCreatePermission];
```
For this tutorial, we've automatically exported all permissions from this file (see `plugins/todo-list-common/src/index.ts`).
> Note: All permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/local-dev/cli-build-system#package-roles). This allows Backstage integrators to reference them in frontend components and permission policies.
> Note: We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/local-dev/cli-build-system#package-roles). This allows Backstage integrators to reference them in frontend components and permission policies.
## Authorizing using the new permission
@@ -47,7 +50,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
- import { InputError } from '@backstage/errors';
+ import { InputError, NotAllowedError } from '@backstage/errors';
+ import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
+ import { todoListCreate } from '@internal/plugin-todo-list-common';
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
...
@@ -72,7 +75,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`:
const user = token ? await identity.authenticate(token) : undefined;
author = user?.identity.userEntityRef;
+ const decision = (
+ await permissions.authorize([{ permission: todoListCreate }], {
+ await permissions.authorize([{ permission: todoListCreatePermission }], {
+ token,
+ })
+ )[0];
@@ -135,7 +138,7 @@ In order to test the logic above, the integrators of your backstage instance nee
+ PolicyQuery,
} from '@backstage/plugin-permission-node';
+ import { isPermission } from '@backstage/plugin-permission-common';
+ import { todoListCreate } from '@internal/plugin-todo-list-common';
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
class TestPermissionPolicy implements PermissionPolicy {
- async handle(): Promise<PolicyDecision> {
@@ -143,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee
+ request: PolicyQuery,
+ user?: BackstageIdentityResponse,
+ ): Promise<PolicyDecision> {
+ if (isPermission(request.permission, todoListCreate)) {
+ if (isPermission(request.permission, todoListCreatePermission)) {
+ return {
+ result: AuthorizeResult.DENY,
+ };
@@ -160,7 +163,7 @@ Now the frontend should show an error whenever you try to create a new Todo item
Let's flip the result back to `ALLOW` before moving on.
```diff
if (isPermission(request.permission, todoListCreate)) {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
- result: AuthorizeResult.DENY,
+ result: AuthorizeResult.ALLOW,
@@ -15,27 +15,30 @@ Let's add a new permission to the file `plugins/todo-list-common/src/permissions
+ export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
+
export const todoListCreate = createPermission({
export const todoListCreatePermission = createPermission({
name: 'todo.list.create',
attributes: { action: 'create' },
});
+
+ export const todoListUpdate = createPermission({
+ export const todoListUpdatePermission = createPermission({
+ name: 'todo.list.update',
+ attributes: { action: 'update' },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
+ });
- export const todoListPermissions = [todoListCreatePermission];
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
```
Notice that unlike `todoListCreate`, the `todoListUpdate` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
## Setting up authorization for the update permission
To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
```diff
- import { todoListCreate } from '@internal/plugin-todo-list-common';
+ import { todoListCreate, todoListUpdate } from '@internal/plugin-todo-list-common';
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
+ import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
...
@@ -49,7 +52,7 @@ To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the sa
}
+ const decision = (
+ await permissions.authorize(
+ [{ permission: todoListUpdate, resourceRef: req.body.id }],
+ [{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
+ {
+ token,
+ },
@@ -119,6 +122,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
- `getResources`: a function that accepts an array of `resourceRefs` in the same format you expect to be passed to `authorize`, and returns an array of the corresponding resources.
- `resourceType`: the same value used in the permission rule above.
- `permissions`: the list of permissions that your plugin accepts.
- `rules`: an array of all the permission rules you want to support in conditional decisions.
```diff
@@ -127,7 +131,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
- import { add, getAll, update } from './todos';
+ import { add, getAll, getTodo, update } from './todos';
+ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
+ import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
+ import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
+ import { rules } from './rules';
export async function createRouter(
@@ -140,6 +144,7 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
+ return resourceRefs.map(getTodo);
+ },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
+ permissions: todoListPermissions,
+ rules: Object.values(rules),
+ });
@@ -196,10 +201,10 @@ Let's go back to the permission policy's handle function and try to authorize ou
PolicyQuery,
} from '@backstage/plugin-permission-node';
import { isPermission } from '@backstage/plugin-permission-common';
- import { todoListCreate } from '@internal/plugin-todo-list-common';
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
+ import {
+ todoListCreate,
+ todoListUpdate,
+ todoListCreatePermission,
+ todoListUpdatePermission,
+ TODO_LIST_RESOURCE_TYPE,
+ } from '@internal/plugin-todo-list-common';
+ import {
@@ -209,13 +214,13 @@ Let's go back to the permission policy's handle function and try to authorize ou
...
if (isPermission(request.permission, todoListCreate)) {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.ALLOW,
};
}
+ if (isPermission(request.permission, todoListUpdate)) {
+ if (isPermission(request.permission, todoListUpdatePermission)) {
+ return createTodoListConditionalDecision(
+ request.permission,
+ todoListConditions.isOwner(user?.identity.userEntityRef),
@@ -42,12 +42,12 @@ Let's add another permission to the plugin.
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
export const todoListCreate = createPermission({
export const todoListCreatePermission = createPermission({
name: 'todo.list.create',
attributes: { action: 'create' },
});
export const todoListUpdate = createPermission({
export const todoListUpdatePermission = createPermission({
name: 'todo.list.update',
attributes: { action: 'update' },
resourceType: TODO_LIST_RESOURCE_TYPE,
@@ -58,6 +58,9 @@ Let's add another permission to the plugin.
+ attributes: { action: 'read' },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
+ });
- export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
```
## Using conditional policy decisions
@@ -126,18 +129,18 @@ Let's update our permission policy to return a conditional result whenever a `to
...
import {
todoListCreate,
todoListUpdate,
+ todoListRead,
todoListCreatePermission,
todoListUpdatePermission,
+ todoListReadPermission,
TODO_LIST_RESOURCE_TYPE,
} from '@internal/plugin-todo-list-common';
...
- if (isPermission(request.permission, todoListUpdate)) {
- if (isPermission(request.permission, todoListUpdatePermission)) {
+ if (
+ isPermission(request.permission, todoListUpdate) ||
+ isPermission(request.permission, todoListRead)
+ isPermission(request.permission, todoListUpdatePermission) ||
+ isPermission(request.permission, todoListReadPermission)
+ ) {
return createTodoListConditionalDecision(
request.permission,
@@ -0,0 +1,167 @@
---
id: 05-frontend-authorization
title: 5. Frontend Components with Authorization
description: Placing frontend components behind authorization
---
In the previous sections, we learned how to protect our plugin's backend API routes with the permission framework. Most routes that return some data to be displayed (such as our `GET /todos` route) need no additional changes on the frontend, as the backend will simply return an empty list or a `404`. However, for UI elements that trigger a mutative action, it's common practice to hide or disable them when a user doesn't have permission.
Take, for example, the "Add" button in our todo list application. When a user clicks this button, the frontend makes a `POST` request to the `/todos` route of our backend. If a user tries to add a todo but is not authorized, they will have no way of knowing this until they perform the action and are faced with an error. This is a poor user experience. We can do better by disabling the add button.
> Note: Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component.
## Using `usePermission`
Let's start by adding the packages we will need:
```
$ yarn workspace @internal/plugin-todo-list \
add @backstage/plugin-permission-react @internal/plugin-todo-list-common
```
Let's make the following changes in `plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx`:
```diff
...
import {
alertApiRef,
discoveryApiRef,
fetchApiRef,
useApi,
} from '@backstage/core-plugin-api';
+ import { usePermission } from '@backstage/plugin-permission-react';
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
...
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
const title = useRef('');
+ const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
return (
<>
<Typography variant="body1">Add todo</Typography>
<Box
component="span"
alignItems="flex-end"
display="flex"
flexDirection="row"
>
<TextField
placeholder="Write something here..."
onChange={e => (title.current = e.target.value)}
/>
- <Button variant="contained" onClick={handleAdd}>
- Add
- </Button>
+ {!loadingPermission && (
+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
+ Add
+ </Button>
+ )}
</Box>
</>
);
}
...
```
Here we are using the [`usePermission` hook](https://backstage.io/docs/reference/plugin-permission-react.usepermission) to communicate with the permission policy and receive a decision on whether this user is authorized to create a todo list item.
It's really that simple! Let's change our policy to test the disabled button:
```diff
// packages/backend/src/plugins/permission.ts
...
if (isPermission(request.permission, todoListCreatePermission)) {
return {
- result: AuthorizeResult.ALLOW,
+ result: AuthorizeResult.DENY,
};
}
...
```
And now you should see that you are not able to create a todo item from the frontend!
## Using `RequirePermission`
Providing a disabled state can be a helpful signal to users, but there may be cases where hiding the element is preferred. For such cases, you can use the provided [`RequirePermission` component](https://backstage.io/docs/reference/plugin-permission-react.requirepermission):
```diff
// plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx
...
import {
alertApiRef,
discoveryApiRef,
fetchApiRef,
useApi,
} from '@backstage/core-plugin-api';
- import { usePermission } from '@backstage/plugin-permission-react';
+ import { RequirePermission } from '@backstage/plugin-permission-react';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
...
export const TodoListPage = () => {
...
<Grid container spacing={3} direction="column">
- <Grid item>
- <AddTodo onAdd={handleAdd} />
- </Grid>
+ <RequirePermission permission={todoListCreatePermission}>
+ <Grid item>
+ <AddTodo onAdd={handleAdd} />
+ </Grid>
+ </RequirePermission>
<Grid item>
<TodoList key={key} onEdit={setEdit} />
</Grid>
</Grid>
...
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
const title = useRef('');
- const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
return (
<>
<Typography variant="body1">Add todo</Typography>
<Box
component="span"
alignItems="flex-end"
display="flex"
flexDirection="row"
>
<TextField
placeholder="Write something here..."
onChange={e => (title.current = e.target.value)}
/>
- {!loadingPermission && (
- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
- Add
- </Button>
- )}
+ <Button variant="contained" onClick={handleAdd}>
+ Add
+ </Button>
</Box>
</>
);
}
...
```
Now you should find that the component for adding a todo list item does not render at all. Success!
+2 -2
View File
@@ -13,10 +13,10 @@ A new, bare-bones backend plugin package can be created by issuing the following
command in your Backstage repository root:
```sh
yarn create-plugin --backend
yarn new --select backend-plugin
```
Please also see the `--help` flag for the `create-plugin` command for some
Please also see the `--help` flag for the `new` command for some
further options that are available, notably the `--scope` and `--no-private`
flags that control naming and publishing of the newly created package. Your repo
root `package.json` will probably also have some default values already set up
+60
View File
@@ -0,0 +1,60 @@
---
id: v1.6.0
title: v1.6.0
description: Backstage Release v1.6.0
---
These are the release notes for the v1.6.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### Moved to `swc` for transpilation
Weve replaced `sucrase` with [`swc`](https://swc.rs/) for transpilation in the Backstage CLI. While `swc` has slightly slower transpilation times, its a library backed by a larger community, and allows us to take advantage of [React Refresh](https://www.npmjs.com/package/react-refresh) out of the box. Theres a few things that could possibly break installations of Backstage and compilation, you can read more about it in the [changelog](https://github.com/backstage/backstage/blob/515aadf8840591860e4bbdcc7d99cef8f9d7ac3c/docs/releases/v1.6.0-changelog.md#patch-changes-1)
### React Router Stable Compatibility
Backstage has for a long time been using React Router version `6.0.0-beta.0`. We adopted this unstable version because v6 had some new features that fit really well with Backstage, particularly relative routing. Because we jumped on this early and unstable version, we knew that we would at some point need a breaking migration to the stable version of React Router v6, which is the point we're at now!
The migration is controlled by each app, meaning this release will not force you to migrate straight away, you can do so at your own pace. Check out the [migration guide](https://backstage.io/docs/tutorials/react-router-stable-migration) for all you need to know!
### Yarn 3 Support
It is now possible to migrate Backstage projects to use Yarn 3. See the [migration guide](https://backstage.io/docs/tutorials/yarn-migration) for more information. Migrating to Yarn 3 is optional, and Backstage projects created with `@backstage/create-app` will still use classic Yarn by default.
### New plugin: `@backstage/plugin-user-settings-backend`
The `user-settings` plugin now has an associated backend. This allows for the persistence of settings in your database, essentially in the form of a basic per-user key-value JSON store.
As this backend was added, `user-settings` also gained a `UserSettingsStore` class that implements the `storageApiRef` Utility API. If you install the backend as well as this frontend API, your starred entities and other storage-API-based features will no longer just be persisted in your browsers local storage, but centrally so that all your devices can leverage them.
Contributed by [@dschwank](https://github.com/dschwank) in [#13570](https://github.com/backstage/backstage/pull/13570)
### New plugin: `@backstage/plugin-playlist`
This plugin can be used to create custom collections of Entities that can be shared throughout Backstage or for private usage.
Contributed by [@kuangp](https://github.com/kuangp) in [#12870](https://github.com/backstage/backstage/pull/12870)
## Security Fixes
Be sure to upgrade to the latest version of `@backstage/plugin-scaffolder-backend`, as it contains an explicit bump of a transitive dependency where a vulnerability was discovered. If you subscribe to CVE notifications you will already have received this update.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.6.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage.
+2 -2
View File
@@ -34,7 +34,7 @@ const Background = props => {
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.MediaFrame>
<iframe
src="https://www.youtube.com/embed/qYnvc8ge1kg"
src="https://www.youtube.com/embed/K44RQAVWWnY"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -45,7 +45,7 @@ const Background = props => {
<iframe
width="300"
height="500"
src="https://www.youtube.com/live_chat?v=qYnvc8ge1kg&embed_domain=backstage.io&dark_theme=1"
src="https://www.youtube.com/live_chat?v=K44RQAVWWnY&embed_domain=backstage.io&dark_theme=1"
></iframe>
</Block.MediaFrame>
</Block.Container>
+5 -2
View File
@@ -1,6 +1,7 @@
{
"releases": {
"Release Notes": [
"releases/v1.6.0",
"releases/v1.5.0",
"releases/v1.4.0",
"releases/v1.3.0",
@@ -96,6 +97,7 @@
"features/software-templates/builtin-actions",
"features/software-templates/writing-custom-actions",
"features/software-templates/writing-custom-field-extensions",
"features/software-templates/writing-custom-step-layouts",
"features/software-templates/migrating-from-v1beta2-to-v1beta3"
]
},
@@ -275,7 +277,6 @@
]
},
"auth/identity-resolver",
"auth/auth-backend",
"auth/oauth",
"auth/add-auth-provider",
"auth/troubleshooting",
@@ -286,6 +287,7 @@
"permissions/concepts",
"permissions/getting-started",
"permissions/writing-a-policy",
"permissions/frontend-integration",
"permissions/custom-rules",
{
"type": "subcategory",
@@ -294,7 +296,8 @@
"permissions/plugin-authors/01-setup",
"permissions/plugin-authors/02-adding-a-basic-permission-check",
"permissions/plugin-authors/03-adding-a-resource-permission-check",
"permissions/plugin-authors/04-authorizing-access-to-paginated-data"
"permissions/plugin-authors/04-authorizing-access-to-paginated-data",
"permissions/plugin-authors/05-frontend-authorization"
]
}
],
+1 -1
View File
@@ -64,6 +64,7 @@ nav:
- Writing Templates: 'features/software-templates/writing-templates.md'
- Builtin Actions: 'features/software-templates/builtin-actions.md'
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md'
- Writing Templates (Legacy): 'features/software-templates/legacy.md'
- Migrating from v1alpha1 to v1beta2 templates: 'features/software-templates/migrating-from-v1alpha1-to-v1beta2.md'
- Backstage Search:
@@ -161,7 +162,6 @@ nav:
- OAuth2Proxy: 'auth/oauth2-proxy/provider.md'
- Bitbucket: 'auth/bitbucket/provider.md'
- Sign in resolvers: 'auth/identity-resolver.md'
- Auth backend: 'auth/auth-backend.md'
- OAuth and OpenID Connect: 'auth/oauth.md'
- Contributing New Providers: 'auth/add-auth-provider.md'
- Troubleshooting Auth: 'auth/troubleshooting.md'
+2 -4
View File
@@ -15,8 +15,8 @@
"tsc": "tsc",
"tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli test",
"test:all": "lerna run test -- --coverage",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"lint": "backstage-cli repo lint --since origin/master",
"lint:docs": "node ./scripts/check-docs-quality",
"lint:all": "backstage-cli repo lint",
@@ -26,7 +26,6 @@
"create-plugin": "echo \"use 'yarn new' instead\"",
"release": "node scripts/prepare-release.js && changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable",
"prettier:check": "prettier --check .",
"lerna": "lerna",
"storybook": "yarn ./storybook run start",
"snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false",
"snyk:test:package": "yarn snyk:test --include",
@@ -71,7 +70,6 @@
"eslint-plugin-notice": "^0.9.10",
"fs-extra": "10.1.0",
"husky": "^8.0.0",
"lerna": "~5.0.0",
"lint-staged": "^13.0.0",
"minimist": "^1.2.5",
"prettier": "^2.2.1",
+7 -7
View File
@@ -32,11 +32,11 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/core-app-api": "^1.1.0",
"@backstage/core-components": "^0.11.1",
"@backstage/core-plugin-api": "^1.0.6",
"@backstage/plugin-permission-react": "^0.4.5",
"@backstage/theme": "^0.2.16",
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1"
},
@@ -47,8 +47,8 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/test-utils": "^1.2.0",
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@types/node": "^16.11.26",
+58 -58
View File
@@ -7,63 +7,63 @@
},
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^1.0.6",
"@backstage/catalog-model": "^1.1.1",
"@backstage/cli": "^0.19.0",
"@backstage/config": "^1.0.2",
"@backstage/core-app-api": "^1.1.0",
"@backstage/core-components": "^0.11.1",
"@backstage/core-plugin-api": "^1.0.6",
"@backstage/integration-react": "^1.1.4",
"@backstage/plugin-airbrake": "^0.3.9",
"@backstage/plugin-apache-airflow": "^0.2.2",
"@backstage/plugin-api-docs": "^0.8.9",
"@backstage/plugin-azure-devops": "^0.2.0",
"@backstage/plugin-azure-functions": "^0.0.0",
"@backstage/plugin-badges": "^0.2.33",
"@backstage/plugin-catalog-common": "^1.0.6",
"@backstage/plugin-catalog-graph": "^0.2.21",
"@backstage/plugin-catalog-import": "^0.8.12",
"@backstage/plugin-catalog-react": "^1.1.4",
"@backstage/plugin-circleci": "^0.3.9",
"@backstage/plugin-cloudbuild": "^0.3.9",
"@backstage/plugin-code-coverage": "^0.2.2",
"@backstage/plugin-cost-insights": "^0.11.31",
"@backstage/plugin-dynatrace": "^0.2.0",
"@backstage/plugin-explore": "^0.3.40",
"@backstage/plugin-gcalendar": "^0.3.5",
"@backstage/plugin-gcp-projects": "^0.3.28",
"@backstage/plugin-github-actions": "^0.5.9",
"@backstage/plugin-gocd": "^0.1.15",
"@backstage/plugin-graphiql": "^0.2.41",
"@backstage/plugin-home": "^0.4.25",
"@backstage/plugin-jenkins": "^0.7.8",
"@backstage/plugin-kafka": "^0.3.9",
"@backstage/plugin-kubernetes": "^0.7.2",
"@backstage/plugin-lighthouse": "^0.3.9",
"@backstage/plugin-newrelic": "^0.3.27",
"@backstage/plugin-newrelic-dashboard": "^0.2.2",
"@backstage/plugin-org": "^0.5.9",
"@backstage/plugin-pagerduty": "0.5.2",
"@backstage/plugin-permission-react": "^0.4.5",
"@backstage/plugin-playlist": "^0.1.0",
"@backstage/plugin-rollbar": "^0.4.9",
"@backstage/plugin-scaffolder": "^1.6.0",
"@backstage/plugin-search": "^1.0.2",
"@backstage/plugin-search-common": "^1.0.1",
"@backstage/plugin-search-react": "^1.1.0",
"@backstage/plugin-sentry": "^0.4.2",
"@backstage/plugin-shortcuts": "^0.3.1",
"@backstage/plugin-stack-overflow": "^0.1.5",
"@backstage/plugin-tech-insights": "^0.3.0",
"@backstage/plugin-tech-radar": "^0.5.16",
"@backstage/plugin-techdocs": "^1.3.2",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.4",
"@backstage/plugin-techdocs-react": "^1.0.4",
"@backstage/plugin-todo": "^0.2.11",
"@backstage/plugin-user-settings": "^0.4.8",
"@backstage/theme": "^0.2.16",
"@internal/plugin-catalog-customized": "0.0.2",
"@backstage/app-defaults": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-airbrake": "workspace:^",
"@backstage/plugin-apache-airflow": "workspace:^",
"@backstage/plugin-api-docs": "workspace:^",
"@backstage/plugin-azure-devops": "workspace:^",
"@backstage/plugin-azure-functions": "workspace:^",
"@backstage/plugin-badges": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-graph": "workspace:^",
"@backstage/plugin-catalog-import": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-circleci": "workspace:^",
"@backstage/plugin-cloudbuild": "workspace:^",
"@backstage/plugin-code-coverage": "workspace:^",
"@backstage/plugin-cost-insights": "workspace:^",
"@backstage/plugin-dynatrace": "workspace:^",
"@backstage/plugin-explore": "workspace:^",
"@backstage/plugin-gcalendar": "workspace:^",
"@backstage/plugin-gcp-projects": "workspace:^",
"@backstage/plugin-github-actions": "workspace:^",
"@backstage/plugin-gocd": "workspace:^",
"@backstage/plugin-graphiql": "workspace:^",
"@backstage/plugin-home": "workspace:^",
"@backstage/plugin-jenkins": "workspace:^",
"@backstage/plugin-kafka": "workspace:^",
"@backstage/plugin-kubernetes": "workspace:^",
"@backstage/plugin-lighthouse": "workspace:^",
"@backstage/plugin-newrelic": "workspace:^",
"@backstage/plugin-newrelic-dashboard": "workspace:^",
"@backstage/plugin-org": "workspace:^",
"@backstage/plugin-pagerduty": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-playlist": "workspace:^",
"@backstage/plugin-rollbar": "workspace:^",
"@backstage/plugin-scaffolder": "workspace:^",
"@backstage/plugin-search": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
"@backstage/plugin-sentry": "workspace:^",
"@backstage/plugin-shortcuts": "workspace:^",
"@backstage/plugin-stack-overflow": "workspace:^",
"@backstage/plugin-tech-insights": "workspace:^",
"@backstage/plugin-tech-radar": "workspace:^",
"@backstage/plugin-techdocs": "workspace:^",
"@backstage/plugin-techdocs-module-addons-contrib": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/plugin-todo": "workspace:^",
"@backstage/plugin-user-settings": "workspace:^",
"@backstage/theme": "workspace:^",
"@internal/plugin-catalog-customized": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -82,7 +82,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^1.2.0",
"@backstage/test-utils": "workspace:^",
"@rjsf/core": "^3.2.1",
"@testing-library/cypress": "^8.0.2",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -148,7 +148,7 @@ const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
<Route path="/" element={<Navigate to="/catalog" />} />
{/* TODO(rubenl): Move this to / once its more mature and components exist */}
<Route path="/home" element={<HomepageCompositionRoot />}>
{homePage}
@@ -28,7 +28,6 @@ const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
return (
<>
<h1>{title}</h1>
<h2>In two column layout!!</h2>
<Grid container justifyContent="flex-end">
{left.map(prop => (
<Grid item xs={6} key={prop.content.key}>
+6 -6
View File
@@ -33,17 +33,17 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/backend-common": "^0.15.1",
"@backstage/backend-plugin-api": "^0.1.2",
"@backstage/backend-tasks": "^0.3.5",
"@backstage/errors": "^1.1.1",
"@backstage/plugin-permission-node": "^0.6.5",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.19.0"
"@backstage/cli": "workspace:^"
},
"files": [
"dist",
+8 -8
View File
@@ -34,12 +34,12 @@
"test:kubernetes": "backstage-cli package test -t KubernetesContainerRunner --no-watch"
},
"dependencies": {
"@backstage/cli-common": "^0.1.10",
"@backstage/config": "^1.0.2",
"@backstage/config-loader": "^1.1.4",
"@backstage/errors": "^1.1.1",
"@backstage/integration": "^1.3.1",
"@backstage/types": "^1.0.0",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/types": "workspace:^",
"@google-cloud/storage": "^6.0.0",
"@keyv/redis": "^2.2.3",
"@kubernetes/client-node": "^0.17.0",
@@ -94,8 +94,8 @@
}
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.28",
"@backstage/cli": "^0.19.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/archiver": "^5.1.0",
"@types/base64-stream": "^1.0.2",
"@types/compression": "^1.7.0",
+3 -3
View File
@@ -32,11 +32,11 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/backend-app-api": "^0.2.1",
"@backstage/backend-plugin-api": "^0.1.2"
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "^0.19.0"
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
+4 -4
View File
@@ -25,12 +25,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/backend-defaults": "^0.1.1",
"@backstage/plugin-catalog-backend": "^1.4.0",
"@backstage/plugin-scaffolder-backend": "^1.6.0"
"@backstage/backend-defaults": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "^0.19.0"
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
+5 -5
View File
@@ -33,17 +33,17 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/backend-common": "^0.15.1",
"@backstage/backend-tasks": "^0.3.5",
"@backstage/config": "^1.0.2",
"@backstage/plugin-permission-common": "^0.6.4",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"winston": "^3.2.1",
"winston-transport": "^4.5.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0"
"@backstage/cli": "workspace:^"
},
"files": [
"dist",
+6 -6
View File
@@ -32,10 +32,10 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/backend-common": "^0.15.1",
"@backstage/config": "^1.0.2",
"@backstage/errors": "^1.1.1",
"@backstage/types": "^1.0.0",
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"@types/luxon": "^3.0.0",
"cron": "^2.0.0",
"knex": "^2.0.0",
@@ -47,8 +47,8 @@
"zod": "^3.9.5"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.28",
"@backstage/cli": "^0.19.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/cron": "^2.0.0",
"wait-for-expect": "^3.0.2"
},
@@ -25,8 +25,6 @@ import {
PluginTaskSchedulerImpl,
} from './PluginTaskSchedulerImpl';
jest.useFakeTimers();
function defer() {
let resolve = () => {};
const promise = new Promise<void>(_resolve => {
@@ -40,6 +38,15 @@ describe('PluginTaskManagerImpl', () => {
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
beforeAll(async () => {
// Make sure all databases are running before mocking timers, in case of testcontainers
await Promise.all(
databases.eachSupportedId().map(([id]) => databases.init(id)),
);
jest.useFakeTimers();
}, 60_000);
async function init(databaseId: TestDatabaseId) {
const knex = await databases.init(databaseId);
await migrateBackendTasks(knex);
+6 -6
View File
@@ -34,11 +34,11 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/backend-app-api": "^0.2.1",
"@backstage/backend-common": "^0.15.1",
"@backstage/backend-plugin-api": "^0.1.2",
"@backstage/cli": "^0.19.0",
"@backstage/config": "^1.0.2",
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"better-sqlite3": "^7.5.0",
"knex": "^2.0.0",
"msw": "^0.47.0",
@@ -48,7 +48,7 @@
"uuid": "^8.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0"
"@backstage/cli": "workspace:^"
},
"files": [
"dist",
@@ -28,6 +28,13 @@ import {
TestDatabaseProperties,
} from './types';
const LARGER_POOL_CONFIG = {
pool: {
min: 0,
max: 50,
},
};
/**
* Encapsulates the creation of ephemeral test database instances for use
* inside unit or integration tests.
@@ -164,6 +171,9 @@ export class TestDatabases {
new ConfigReader({
backend: {
database: {
knexConfig: properties.driver.includes('sqlite')
? {}
: LARGER_POOL_CONFIG,
client: properties.driver,
connection: connectionString,
},
@@ -203,6 +213,7 @@ export class TestDatabases {
new ConfigReader({
backend: {
database: {
knexConfig: LARGER_POOL_CONFIG,
client: 'pg',
connection: { host, port, user, password },
},
@@ -228,6 +239,7 @@ export class TestDatabases {
new ConfigReader({
backend: {
database: {
knexConfig: LARGER_POOL_CONFIG,
client: 'mysql2',
connection: { host, port, user, password },
},
+37 -37
View File
@@ -26,42 +26,42 @@
"build-image": "docker build ../.. -f Dockerfile --tag example-backend"
},
"dependencies": {
"@backstage/backend-common": "^0.15.1",
"@backstage/backend-tasks": "^0.3.5",
"@backstage/catalog-client": "^1.1.0",
"@backstage/catalog-model": "^1.1.1",
"@backstage/config": "^1.0.2",
"@backstage/integration": "^1.3.1",
"@backstage/plugin-app-backend": "^0.3.36",
"@backstage/plugin-auth-backend": "^0.16.0",
"@backstage/plugin-auth-node": "^0.2.5",
"@backstage/plugin-azure-devops-backend": "^0.3.15",
"@backstage/plugin-azure-functions-backend": "^0.0.0",
"@backstage/plugin-badges-backend": "^0.1.30",
"@backstage/plugin-catalog-backend": "^1.4.0",
"@backstage/plugin-code-coverage-backend": "^0.2.2",
"@backstage/plugin-graphql-backend": "^0.1.26",
"@backstage/plugin-jenkins-backend": "^0.1.26",
"@backstage/plugin-kafka-backend": "^0.2.29",
"@backstage/plugin-kubernetes-backend": "^0.7.2",
"@backstage/plugin-permission-backend": "^0.5.11",
"@backstage/plugin-permission-common": "^0.6.4",
"@backstage/plugin-permission-node": "^0.6.5",
"@backstage/plugin-playlist-backend": "^0.1.0",
"@backstage/plugin-proxy-backend": "^0.2.30",
"@backstage/plugin-rollbar-backend": "^0.1.33",
"@backstage/plugin-scaffolder-backend": "^1.6.0",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.4.4",
"@backstage/plugin-search-backend": "^1.0.2",
"@backstage/plugin-search-backend-module-elasticsearch": "^1.0.2",
"@backstage/plugin-search-backend-module-pg": "^0.4.0",
"@backstage/plugin-search-backend-node": "^1.0.2",
"@backstage/plugin-search-common": "^1.0.1",
"@backstage/plugin-tech-insights-backend": "^0.5.2",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.20",
"@backstage/plugin-tech-insights-node": "^0.3.4",
"@backstage/plugin-techdocs-backend": "^1.3.0",
"@backstage/plugin-todo-backend": "^0.1.33",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-azure-devops-backend": "workspace:^",
"@backstage/plugin-azure-functions-backend": "workspace:^",
"@backstage/plugin-badges-backend": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-code-coverage-backend": "workspace:^",
"@backstage/plugin-graphql-backend": "workspace:^",
"@backstage/plugin-jenkins-backend": "workspace:^",
"@backstage/plugin-kafka-backend": "workspace:^",
"@backstage/plugin-kubernetes-backend": "workspace:^",
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-playlist-backend": "workspace:^",
"@backstage/plugin-proxy-backend": "workspace:^",
"@backstage/plugin-rollbar-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-rails": "workspace:^",
"@backstage/plugin-search-backend": "workspace:^",
"@backstage/plugin-search-backend-module-elasticsearch": "workspace:^",
"@backstage/plugin-search-backend-module-pg": "workspace:^",
"@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-tech-insights-backend": "workspace:^",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "workspace:^",
"@backstage/plugin-tech-insights-node": "workspace:^",
"@backstage/plugin-techdocs-backend": "workspace:^",
"@backstage/plugin-todo-backend": "workspace:^",
"@gitbeaker/node": "^35.1.0",
"@octokit/rest": "^19.0.3",
"azure-devops-node-api": "^11.0.1",
@@ -78,7 +78,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
@@ -33,5 +33,6 @@ export default async function createPlugin(
catalogClient: catalogClient,
reader: env.reader,
identity: env.identity,
scheduler: env.scheduler,
});
}
+3 -3
View File
@@ -32,12 +32,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/catalog-model": "^1.1.1",
"@backstage/errors": "^1.1.1",
"@backstage/catalog-model": "workspace:^",
"@backstage/errors": "workspace:^",
"cross-fetch": "^3.1.5"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"msw": "^0.47.0"
},
"files": [
+4 -4
View File
@@ -33,16 +33,16 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/config": "^1.0.2",
"@backstage/errors": "^1.1.1",
"@backstage/types": "^1.0.0",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"ajv": "^8.10.0",
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"uuid": "^8.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/json-schema": "^7.0.5",
"@types/lodash": "^4.14.151",
"yaml": "^2.0.0"
+4 -1
View File
@@ -22,9 +22,12 @@ import { KindValidator } from './types';
// exported kind validators have the `KindValidator` signature which is
// different. So let's postpone that change until a later time.
export function ajvCompiledJsonSchemaValidator(schema: unknown): KindValidator {
const validator = entityKindSchemaValidator(schema);
let validator: undefined | ((data: unknown) => any);
return {
async check(data) {
if (!validator) {
validator = entityKindSchemaValidator(schema);
}
return validator(data) === data;
},
};
+1 -1
View File
@@ -32,7 +32,7 @@
"start": "backstage-cli package start"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/node": "^16.0.0"
},
"files": [
+18 -8
View File
@@ -276,7 +276,7 @@ Usage: backstage-cli [--config=<pathToConfigFile>] [TestPathPattern]
Options:
-h, --help
-v, --version
--version
--all
--automock
-b, --bail
@@ -289,7 +289,6 @@ Options:
--clearMocks
--collectCoverage
--collectCoverageFrom
--collectCoverageOnlyFrom
--color
--colors
-c, --config
@@ -312,6 +311,7 @@ Options:
--globalTeardown
--globals
--haste
--ignoreProjects
--init
--injectGlobals
--json
@@ -348,6 +348,7 @@ Options:
--selectProjects
--setupFiles
--setupFilesAfterEnv
--shard
--showConfig
--silent
--skipFilter
@@ -365,8 +366,6 @@ Options:
--testRunner
--testSequencer
--testTimeout
--testURL
--timers
--transform
--transformIgnorePatterns
--unmockedModulePathPatterns
@@ -391,6 +390,7 @@ Commands:
build [options]
lint [options]
clean
test [options]
help [command]
```
@@ -426,6 +426,17 @@ Options:
-h, --help
```
### `backstage-cli repo test`
```
Usage: backstage-cli repo test [options]
Options:
--since <ref>
--jest-help
-h, --help
```
### `backstage-cli test`
```
@@ -433,7 +444,7 @@ Usage: backstage-cli [--config=<pathToConfigFile>] [TestPathPattern]
Options:
-h, --help
-v, --version
--version
--all
--automock
-b, --bail
@@ -446,7 +457,6 @@ Options:
--clearMocks
--collectCoverage
--collectCoverageFrom
--collectCoverageOnlyFrom
--color
--colors
-c, --config
@@ -469,6 +479,7 @@ Options:
--globalTeardown
--globals
--haste
--ignoreProjects
--init
--injectGlobals
--json
@@ -505,6 +516,7 @@ Options:
--selectProjects
--setupFiles
--setupFilesAfterEnv
--shard
--showConfig
--silent
--skipFilter
@@ -522,8 +534,6 @@ Options:
--testRunner
--testSequencer
--testTimeout
--testURL
--timers
--transform
--transformIgnorePatterns
--unmockedModulePathPatterns
+6 -7
View File
@@ -27,7 +27,6 @@ const envOptions = {
const transformIgnorePattern = [
'@material-ui',
'@rjsf',
'ajv',
'core-js',
'jest-.*',
@@ -189,13 +188,13 @@ async function getProjectConfig(targetPath, displayName) {
],
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$':
require.resolve('./jestFileTransform.js'),
'\\.(yaml)$': require.resolve('jest-transform-yaml'),
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
},
// A bit more opinionated
testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'],
moduleLoader: envOptions.nextTests
runtime: envOptions.nextTests
? require.resolve('./jestCachingModuleLoader')
: undefined,
@@ -210,16 +209,16 @@ async function getProjectConfig(targetPath, displayName) {
const config = Object.assign(options, ...pkgJsonConfigs);
// The config name is a cache key that lets us share the jest cache across projects.
// If no explicit name was configured, generated one based on the configuration.
if (!config.name) {
// The config id is a cache key that lets us share the jest cache across projects.
// If no explicit id was configured, generated one based on the configuration.
if (!config.id) {
const configHash = crypto
.createHash('md5')
.update(version)
.update(Buffer.alloc(1))
.update(JSON.stringify(config.transform))
.digest('hex');
config.name = `backstage_cli_${configHash}`;
config.id = `backstage_cli_${configHash}`;
}
return config;
+5 -3
View File
@@ -21,7 +21,8 @@ module.exports = {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.icon\.svg$/)) {
return `const React = require('react');
return {
code: `const React = require('react');
const SvgIcon = require('@material-ui/core/SvgIcon').default;
module.exports = {
__esModule: true,
@@ -34,9 +35,10 @@ module.exports = {
children: ${assetFilename}
})
})
};`;
};`,
};
}
return `module.exports = ${assetFilename};`;
return { code: `module.exports = ${assetFilename};` };
},
};
+2 -2
View File
@@ -64,10 +64,10 @@ function createTransformer(config) {
}
// We only return the `map` result if source maps are enabled, as they
// have a negative impact on the coverage accuracy.
return code;
return { code };
}
return source;
return { code: source };
};
// TODO: contribute something like this to @sucrase/jest-plugin
+1 -1
View File
@@ -21,7 +21,7 @@ function createTransformer(config) {
const swcTransformer = createSwcTransformer(config);
const process = (source, filePath, jestOptions) => {
if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) {
return source;
return { code: source };
}
return swcTransformer.process(source, filePath, jestOptions);
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const yaml = require('yaml');
const crypto = require('crypto');
function createTransformer(config) {
const process = source => {
const json = JSON.stringify(yaml.parse(source), null, 2);
return { code: `module.exports = ${json}`, map: null };
};
const getCacheKey = sourceText => {
return crypto
.createHash('md5')
.update(sourceText)
.update(Buffer.alloc(1))
.update(JSON.stringify(config))
.update(Buffer.alloc(1))
.update('1') // increment whenever the transform logic in this file changes
.digest('hex');
};
return { process, getCacheKey };
}
module.exports = { createTransformer };
+18 -18
View File
@@ -30,12 +30,12 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.10",
"@backstage/config": "^1.0.2",
"@backstage/config-loader": "^1.1.4",
"@backstage/errors": "^1.1.1",
"@backstage/release-manifests": "^0.0.6",
"@backstage/types": "^1.0.0",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/release-manifests": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@octokit/request": "^6.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
@@ -55,7 +55,7 @@
"@swc/core": "^1.2.239",
"@swc/helpers": "^0.4.7",
"@swc/jest": "^0.2.22",
"@types/jest": "^27",
"@types/jest": "^29.0.0",
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
@@ -89,10 +89,10 @@
"handlebars": "^4.7.3",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^8.2.0",
"jest": "^27.5.1",
"jest": "^29.0.2",
"jest-css-modules": "^2.1.0",
"jest-runtime": "^27.5.1",
"jest-transform-yaml": "^1.0.0",
"jest-environment-jsdom": "^29.0.2",
"jest-runtime": "^29.0.2",
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.2",
@@ -129,14 +129,14 @@
"zod": "^3.11.6"
},
"devDependencies": {
"@backstage/backend-common": "^0.15.1",
"@backstage/config": "^1.0.2",
"@backstage/core-app-api": "^1.1.0",
"@backstage/core-components": "^0.11.1",
"@backstage/core-plugin-api": "^1.0.6",
"@backstage/dev-utils": "^1.0.6",
"@backstage/test-utils": "^1.2.0",
"@backstage/theme": "^0.2.16",
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@backstage/theme": "workspace:^",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
+14
View File
@@ -72,6 +72,20 @@ export function registerRepoCommand(program: Command) {
.action(
lazy(() => import('./repo/list-deprecations').then(m => m.command)),
);
command
.command('test')
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
.option(
'--since <ref>',
'Only test packages that changed since the specified ref',
)
.option(
'--jest-help',
'Show help for Jest CLI options, which are passed through',
)
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazy(() => import('./repo/test').then(m => m.command)));
}
export function registerScriptCommand(program: Command) {
+134
View File
@@ -0,0 +1,134 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command, OptionValues } from 'commander';
import { PackageGraph } from '../../lib/monorepo';
import { paths } from '../../lib/paths';
import { runCheck } from '../../lib/run';
function includesAnyOf(hayStack: string[], ...needles: string[]) {
for (const needle of needles) {
if (hayStack.includes(needle)) {
return true;
}
}
return false;
}
function removeOptionArg(args: string[], option: string) {
let changed = false;
do {
changed = false;
const index = args.indexOf(option);
if (index >= 0) {
changed = true;
args.splice(index, 2);
}
const indexEq = args.findIndex(arg => arg.startsWith(`${option}=`));
if (indexEq >= 0) {
changed = true;
args.splice(indexEq, 1);
}
} while (changed);
}
export async function command(opts: OptionValues, cmd: Command): Promise<void> {
// all args are forwarded to jest
let parent = cmd;
while (parent.parent) {
parent = parent.parent;
}
const allArgs = parent.args as string[];
const args = allArgs.slice(allArgs.indexOf('test') + 1);
// Only include our config if caller isn't passing their own config
if (!includesAnyOf(args, '-c', '--config')) {
args.push('--config', paths.resolveOwn('config/jest.js'));
}
if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) {
args.push('--passWithNoTests');
}
// Run in watch mode unless in CI, coverage mode, or running all tests
if (
!process.env.CI &&
!args.includes('--coverage') &&
// explicitly no watching
!includesAnyOf(args, '--no-watch', '--watch=false', '--watchAll=false') &&
// already watching
!includesAnyOf(args, '--watch', '--watchAll')
) {
const isGitRepo = () =>
runCheck('git', 'rev-parse', '--is-inside-work-tree');
const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root');
if ((await isGitRepo()) || (await isMercurialRepo())) {
args.push('--watch');
} else {
args.push('--watchAll');
}
}
if (opts.since) {
removeOptionArg(args, '--since');
}
if (opts.since && !args.some(arg => arg.startsWith('--selectProjects'))) {
const packages = await PackageGraph.listTargetPackages();
const graph = PackageGraph.fromPackages(packages);
const changedPackages = await graph.listChangedPackages({
ref: opts.since,
});
const packageNames = Array.from(
graph.collectPackageNames(
changedPackages.map(pkg => pkg.name),
pkg => pkg.allLocalDependents.keys(),
),
);
if (packageNames.length === 0) {
console.log(`No packages changed since ${opts.since}`);
return;
}
args.push('--selectProjects', ...packageNames);
}
// This is the only thing that is not implemented by jest.run(), so we do it here instead
// https://github.com/facebook/jest/blob/cd8828f7bbec6e55b4df5e41e853a5133c4a3ee1/packages/jest-cli/bin/jest.js#L12
if (!process.env.NODE_ENV) {
(process.env as any).NODE_ENV = 'test';
}
// This is to have a consistent timezone for when running tests that involve checking
// the formatting of date/times.
// https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config
if (!process.env.TZ) {
process.env.TZ = 'UTC';
}
// This ensures that the process doesn't exit too early before stdout is flushed
if (args.includes('--jest-help')) {
removeOptionArg(args, '--jest-help');
args.push('--help');
(process.stdout as any)._handle.setBlocking(true);
}
await require('jest').run(args);
}
+1
View File
@@ -78,6 +78,7 @@ export default async (_opts: OptionValues, cmd: Command) => {
process.env.TZ = 'UTC';
}
// This ensures that the process doesn't exit too early before stdout is flushed
if (args.includes('--help')) {
(process.stdout as any)._handle.setBlocking(true);
}
+1 -1
View File
@@ -267,7 +267,7 @@ export async function createBackendConfig(
paths.targetRunFile ? paths.targetRunFile : paths.targetEntry,
],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
extensions: ['.ts', '.mjs', '.js', '.json'],
mainFields: ['main'],
modules: [paths.rootNodeModules, ...moduleDirs],
plugins: [
+8
View File
@@ -81,6 +81,14 @@ export function createPackageVersionProvider(lockfile?: Lockfile) {
) {
return '*';
}
for (const specifier of ['^', '~', '*']) {
const range = `workspace:${specifier}`;
if (lockfileEntries?.some(entry => entry.range === range)) {
return range;
}
}
const validRanges = lockfileEntries?.filter(entry =>
semver.satisfies(targetVersion, entry.range),
);
@@ -170,7 +170,7 @@ a@^1:
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
b@2.0.x:
"b@2.0.x, b@^2.0.1":
version: 2.0.1
b@^2:
@@ -185,7 +185,7 @@ a@^1:
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
b@2.0.x:
"b@2.0.x, b@^2.0.1":
version: 2.0.1
b@^2:
@@ -206,6 +206,7 @@ describe('New Lockfile', () => {
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2.0.1', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
]);
expect(lockfile.toString()).toBe(mockANew);
+10 -2
View File
@@ -122,7 +122,7 @@ export class Lockfile {
for (const [key, value] of Object.entries(data)) {
if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
const [, name, range] = ENTRY_PATTERN.exec(key) ?? [];
const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? [];
if (!name) {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
@@ -132,7 +132,15 @@ export class Lockfile {
queries = [];
packages.set(name, queries);
}
queries.push({ range, version: value.version });
for (let range of ranges.split(/\s*,\s*/)) {
if (range.startsWith(`${name}@`)) {
range = range.slice(`${name}@`.length);
}
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version });
}
}
return new Lockfile(path, packages, data, legacy);
+2
View File
@@ -0,0 +1,2 @@
x:
y: 'z'
+23
View File
@@ -0,0 +1,23 @@
/*
* 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 yaml from './yaml-fixture.yaml';
describe('tests', () => {
it('should load yaml', () => {
expect(yaml).toEqual({ x: { y: 'z' } });
});
});
@@ -30,18 +30,18 @@
"dependencies": {
"@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}",
"@backstage/config": "{{versionQuery '@backstage/config'}}",
"@types/express": "{{versionQuery '@types/express' '4.17.6'}}",
"express": "{{versionQuery 'express' '4.17.1'}}",
"@types/express": "{{versionQuery '@types/express' '4.17.13'}}",
"express": "{{versionQuery 'express' '4.18.1'}}",
"express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}",
"winston": "{{versionQuery 'winston' '3.2.1'}}",
"winston": "{{versionQuery 'winston' '3.8.1'}}",
"node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}",
"yn": "{{versionQuery 'yn' '4.0.0'}}"
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}",
"supertest": "{{versionQuery 'supertest' '4.0.2'}}",
"msw": "{{versionQuery 'msw' '0.46.0'}}"
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}",
"supertest": "{{versionQuery 'supertest' '6.2.4'}}",
"msw": "{{versionQuery 'msw' '0.47.0'}}"
},
"files": [
"dist"
@@ -48,7 +48,7 @@
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}",
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
"@types/node": "{{versionQuery '@types/node' '16.11.26'}}",
"msw": "{{versionQuery 'msw' '0.46.0'}}",
"msw": "{{versionQuery 'msw' '0.47.0'}}",
"cross-fetch": "{{versionQuery 'cross-fetch' '3.1.5'}}"
},
"files": [
+2 -2
View File
@@ -33,13 +33,13 @@
"backstage-codemods": "bin/backstage-codemods"
},
"dependencies": {
"@backstage/cli-common": "^0.1.10",
"@backstage/cli-common": "workspace:^",
"chalk": "^4.0.0",
"jscodeshift": "^0.13.0",
"jscodeshift-add-imports": "^1.0.10"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/jscodeshift": "^0.11.0",
"@types/node": "^16.11.26",
"commander": "^9.1.0",
+5 -5
View File
@@ -33,10 +33,10 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/cli-common": "^0.1.10",
"@backstage/config": "^1.0.2",
"@backstage/errors": "^1.1.1",
"@backstage/types": "^1.0.0",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"@types/json-schema": "^7.0.6",
"ajv": "^8.10.0",
"chokidar": "^3.5.2",
@@ -50,7 +50,7 @@
"yup": "^0.32.9"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/json-schema-merge-allof": "^0.6.0",
"@types/mock-fs": "^4.10.0",
"@types/node": "^16.11.26",
+3 -3
View File
@@ -32,12 +32,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/types": "^1.0.0",
"@backstage/types": "workspace:^",
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/test-utils": "^1.2.0",
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/node": "^16.0.0"
},
"files": [
+6 -6
View File
@@ -32,10 +32,10 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/config": "^1.0.2",
"@backstage/core-plugin-api": "^1.0.6",
"@backstage/types": "^1.0.0",
"@backstage/version-bridge": "^1.0.1",
"@backstage/config": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@types/prop-types": "^15.7.3",
"prop-types": "^15.7.2",
"react-use": "^17.2.4",
@@ -48,8 +48,8 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/test-utils": "^1.2.0",
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
@@ -118,9 +118,10 @@ describe('ApiProvider', () => {
}).toThrow(/^API context is not available/);
}).error,
).toEqual([
expect.stringMatching(
/^Error: Uncaught \[Error: API context is not available/,
),
expect.objectContaining({
detail: new Error('API context is not available'),
type: 'unhandled exception',
}),
expect.stringMatching(
/^The above error occurred in the <MyHookConsumer> component/,
),
@@ -133,9 +134,10 @@ describe('ApiProvider', () => {
}).toThrow(/^API context is not available/);
}).error,
).toEqual([
expect.stringMatching(
/^Error: Uncaught \[Error: API context is not available/,
),
expect.objectContaining({
detail: new Error('API context is not available'),
type: 'unhandled exception',
}),
expect.stringMatching(
/^The above error occurred in the <withApis\(Component\)> component/,
),
@@ -154,9 +156,10 @@ describe('ApiProvider', () => {
}).toThrow('No implementation available for apiRef{x}');
}).error,
).toEqual([
expect.stringMatching(
/^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/,
),
expect.objectContaining({
detail: new Error('No implementation available for apiRef{x}'),
type: 'unhandled exception',
}),
expect.stringMatching(
/^The above error occurred in the <MyHookConsumer> component/,
),
@@ -173,9 +176,10 @@ describe('ApiProvider', () => {
}).toThrow('No implementation available for apiRef{x}');
}).error,
).toEqual([
expect.stringMatching(
/^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/,
),
expect.objectContaining({
detail: new Error('No implementation available for apiRef{x}'),
type: 'unhandled exception',
}),
expect.stringMatching(
/^The above error occurred in the <withApis\(Component\)> component/,
),
+8 -8
View File
@@ -32,11 +32,11 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/config": "^1.0.2",
"@backstage/core-plugin-api": "^1.0.6",
"@backstage/errors": "^1.1.1",
"@backstage/theme": "^0.2.16",
"@backstage/version-bridge": "^1.0.1",
"@backstage/config": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@material-table/core": "^3.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -78,9 +78,9 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/core-app-api": "^1.1.0",
"@backstage/test-utils": "^1.2.0",
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
@@ -47,9 +47,11 @@ describe('TabbedLayout', () => {
});
expect(error).toEqual([
expect.stringMatching(
/Child of TabbedLayout must be an TabbedLayout.Route/,
),
expect.objectContaining({
detail: new Error(
'Child of TabbedLayout must be an TabbedLayout.Route',
),
}),
expect.stringMatching(
/The above error occurred in the <TabbedLayout> component/,
),
@@ -67,7 +67,9 @@ describe('<ErrorBoundary/>', () => {
});
expect(error).toEqual([
expect.stringMatching(/^Error: Uncaught \[Error: Bomb\]/),
expect.objectContaining({
detail: new Error('Bomb'),
}),
expect.stringMatching(
/^The above error occurred in the <Bomb> component:/,
),
@@ -29,7 +29,7 @@ const validBackstageToken =
describe('ProxiedSignInIdentity', () => {
describe('tokenToExpiry', () => {
beforeEach(() => jest.useFakeTimers('modern'));
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('handles undefined', async () => {
@@ -56,7 +56,7 @@ describe('ProxiedSignInIdentity', () => {
});
describe('ProxiedSignInIdentity', () => {
beforeEach(() => jest.useFakeTimers('modern'));
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
const worker = setupServer();
+6 -6
View File
@@ -33,9 +33,9 @@
"start": "backstage-cli package start"
},
"dependencies": {
"@backstage/config": "^1.0.2",
"@backstage/types": "^1.0.0",
"@backstage/version-bridge": "^1.0.1",
"@backstage/config": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"zen-observable": "^0.8.15"
@@ -46,9 +46,9 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/core-app-api": "^1.1.0",
"@backstage/test-utils": "^1.2.0",
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
@@ -117,7 +117,7 @@ describe('extensions', () => {
render(<BrokenComponent />);
});
screen.getByText('Error in my-plugin');
expect(errors[0]).toMatch('Test error');
expect(errors[0]).toMatchObject({ detail: new Error('Test error') });
});
it('should wrap extended component with analytics context', async () => {
+4 -3
View File
@@ -29,10 +29,10 @@
"clean": "backstage-cli package clean",
"prepack": "node scripts/prepack.js",
"postpack": "node scripts/postpack.js",
"start": "nodemon --"
"start": "yarn nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.10",
"@backstage/cli-common": "workspace:^",
"chalk": "^4.0.0",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
@@ -42,12 +42,13 @@
"recursive-readdir": "^2.2.2"
},
"devDependencies": {
"@backstage/cli": "^0.19.0",
"@backstage/cli": "workspace:^",
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^8.1.3",
"@types/node": "^16.11.26",
"@types/recursive-readdir": "^2.2.0",
"mock-fs": "^5.1.1",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
},
"nodemonConfig": {
+11 -11
View File
@@ -28,17 +28,6 @@ jest.mock('./lib/versions', () => ({
packageVersions: { root: '1.0.0' },
}));
beforeAll(() => {
mockFs({
[`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)`
'templates/': mockFs.load(path.resolve(__dirname, '../templates/')),
});
});
afterAll(() => {
mockFs.restore();
});
const promptMock = jest.spyOn(inquirer, 'prompt');
const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask');
const templatingMock = jest.spyOn(tasks, 'templatingTask');
@@ -51,6 +40,17 @@ const moveAppMock = jest.spyOn(tasks, 'moveAppTask');
const buildAppMock = jest.spyOn(tasks, 'buildAppTask');
describe('command entrypoint', () => {
beforeAll(() => {
mockFs({
[`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)`
'templates/': mockFs.load(path.resolve(__dirname, '../templates/')),
});
});
afterAll(() => {
mockFs.restore();
});
beforeEach(() => {
promptMock.mockResolvedValueOnce({
name: 'MyApp',
+179 -173
View File
@@ -86,185 +86,191 @@ jest.mock('./versions', () => ({
},
}));
beforeEach(() => {
mockFs({
'projects/my-module.ts': '',
'projects/dir/my-file.txt': '',
'tmp/mockApp/.gitignore': '',
'tmp/mockApp/package.json': '',
'tmp/mockApp/packages/app/package.json': '',
// load templates into mock filesystem
'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')),
});
});
afterEach(() => {
mockFs.restore();
});
describe('checkAppExistsTask', () => {
it('should do nothing if the directory does not exist', async () => {
const dir = 'projects/';
const name = 'MyNewApp';
await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow();
describe('tasks', () => {
beforeEach(() => {
mockFs({
'projects/my-module.ts': '',
'projects/dir/my-file.txt': '',
'tmp/mockApp/.gitignore': '',
'tmp/mockApp/package.json': '',
'tmp/mockApp/packages/app/package.json': '',
// load templates into mock filesystem
'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')),
});
});
it('should throw an error when a file of the same name exists', async () => {
const dir = 'projects/';
const name = 'my-module.ts';
await expect(checkAppExistsTask(dir, name)).rejects.toThrow(
'already exists',
);
afterEach(() => {
mockFs.restore();
});
it('should throw an error when a directory of the same name exists', async () => {
const dir = 'projects/';
const name = 'dir';
await expect(checkAppExistsTask(dir, name)).rejects.toThrow(
'already exists',
);
});
});
describe('checkPathExistsTask', () => {
it('should create a directory at the given path', async () => {
const appDir = 'projects/newProject';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should do nothing if a directory of the same name exists', async () => {
const appDir = 'projects/dir';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should fail if a file of the same name exists', async () => {
await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow(
'already exists',
);
});
});
describe('createTemporaryAppFolderTask', () => {
it('should create a directory at a given path', async () => {
const tempDir = 'projects/tmpFolder';
await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow();
expect(fs.existsSync(tempDir)).toBe(true);
});
it('should fail if a directory of the same name exists', async () => {
const tempDir = 'projects/dir';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
it('should fail if a file of the same name exists', async () => {
const tempDir = 'projects/dir/my-file.txt';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
});
describe('buildAppTask', () => {
it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => {
const mockChdir = jest.spyOn(process, 'chdir');
const mockExec = child_process.exec as unknown as jest.MockedFunction<
(
command: string,
callback: (error: null, stdout: string, stderr: string) => void,
) => void
>;
// requires callback implementation to support `promisify` wrapper
// https://stackoverflow.com/a/60579617/10044859
mockExec.mockImplementation((_command, callback) => {
callback(null, 'standard out', 'standard error');
describe('checkAppExistsTask', () => {
it('should do nothing if the directory does not exist', async () => {
const dir = 'projects/';
const name = 'MyNewApp';
await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow();
});
const appDir = 'projects/dir';
await expect(buildAppTask(appDir)).resolves.not.toThrow();
expect(mockChdir).toHaveBeenCalledTimes(2);
expect(mockChdir).toHaveBeenNthCalledWith(1, appDir);
expect(mockChdir).toHaveBeenNthCalledWith(2, appDir);
expect(mockExec).toHaveBeenCalledTimes(2);
expect(mockExec).toHaveBeenNthCalledWith(
1,
'yarn install',
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
2,
'yarn tsc',
expect.any(Function),
);
});
it('should fail if project directory does not exist', async () => {
const appDir = 'projects/missingProject';
await expect(buildAppTask(appDir)).rejects.toThrow(
'no such file or directory',
);
});
});
describe('moveAppTask', () => {
const tempDir = 'tmp/mockApp/';
const id = 'myApp';
it('should move all files in the temp dir to the target dir', async () => {
const destination = 'projects/mockApp';
await moveAppTask(tempDir, destination, id);
expect(fs.existsSync('projects/mockApp/.gitignore')).toBe(true);
expect(fs.existsSync('projects/mockApp/package.json')).toBe(true);
expect(fs.existsSync('projects/mockApp/packages/app/package.json')).toBe(
true,
);
});
it('should fail to move files if destination already exists', async () => {
const destination = 'projects';
await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow(
'dest already exists',
);
});
it('should remove temporary files if move succeeded', async () => {
const destination = 'projects/mockApp';
await moveAppTask(tempDir, destination, id);
expect(fs.existsSync('tmp/mockApp')).toBe(false);
});
it('should remove temporary files if move failed', async () => {
const destination = 'projects';
await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow();
expect(fs.existsSync('tmp/mockApp')).toBe(false);
});
});
describe('templatingTask', () => {
it('should generate a project populating context parameters', async () => {
const templateDir = 'templates/default-app';
const destinationDir = 'templatedApp';
const context = {
name: 'SuperCoolBackstageInstance',
dbTypeSqlite: true,
};
await templatingTask(templateDir, destinationDir, context);
expect(fs.existsSync('templatedApp/package.json')).toBe(true);
expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true);
await expect(fs.readJson('templatedApp/backstage.json')).resolves.toEqual({
version: '1.2.3',
it('should throw an error when a file of the same name exists', async () => {
const dir = 'projects/';
const name = 'my-module.ts';
await expect(checkAppExistsTask(dir, name)).rejects.toThrow(
'already exists',
);
});
it('should throw an error when a directory of the same name exists', async () => {
const dir = 'projects/';
const name = 'dir';
await expect(checkAppExistsTask(dir, name)).rejects.toThrow(
'already exists',
);
});
});
describe('checkPathExistsTask', () => {
it('should create a directory at the given path', async () => {
const appDir = 'projects/newProject';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should do nothing if a directory of the same name exists', async () => {
const appDir = 'projects/dir';
await expect(checkPathExistsTask(appDir)).resolves.not.toThrow();
expect(fs.existsSync(appDir)).toBe(true);
});
it('should fail if a file of the same name exists', async () => {
await expect(
checkPathExistsTask('projects/my-module.ts'),
).rejects.toThrow('already exists');
});
});
describe('createTemporaryAppFolderTask', () => {
it('should create a directory at a given path', async () => {
const tempDir = 'projects/tmpFolder';
await expect(
createTemporaryAppFolderTask(tempDir),
).resolves.not.toThrow();
expect(fs.existsSync(tempDir)).toBe(true);
});
it('should fail if a directory of the same name exists', async () => {
const tempDir = 'projects/dir';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
it('should fail if a file of the same name exists', async () => {
const tempDir = 'projects/dir/my-file.txt';
await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow(
'file already exists',
);
});
});
describe('buildAppTask', () => {
it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => {
const mockChdir = jest.spyOn(process, 'chdir');
const mockExec = child_process.exec as unknown as jest.MockedFunction<
(
command: string,
callback: (error: null, stdout: string, stderr: string) => void,
) => void
>;
// requires callback implementation to support `promisify` wrapper
// https://stackoverflow.com/a/60579617/10044859
mockExec.mockImplementation((_command, callback) => {
callback(null, 'standard out', 'standard error');
});
const appDir = 'projects/dir';
await expect(buildAppTask(appDir)).resolves.not.toThrow();
expect(mockChdir).toHaveBeenCalledTimes(2);
expect(mockChdir).toHaveBeenNthCalledWith(1, appDir);
expect(mockChdir).toHaveBeenNthCalledWith(2, appDir);
expect(mockExec).toHaveBeenCalledTimes(2);
expect(mockExec).toHaveBeenNthCalledWith(
1,
'yarn install',
expect.any(Function),
);
expect(mockExec).toHaveBeenNthCalledWith(
2,
'yarn tsc',
expect.any(Function),
);
});
it('should fail if project directory does not exist', async () => {
const appDir = 'projects/missingProject';
await expect(buildAppTask(appDir)).rejects.toThrow(
'no such file or directory',
);
});
});
describe('moveAppTask', () => {
const tempDir = 'tmp/mockApp/';
const id = 'myApp';
it('should move all files in the temp dir to the target dir', async () => {
const destination = 'projects/mockApp';
await moveAppTask(tempDir, destination, id);
expect(fs.existsSync('projects/mockApp/.gitignore')).toBe(true);
expect(fs.existsSync('projects/mockApp/package.json')).toBe(true);
expect(fs.existsSync('projects/mockApp/packages/app/package.json')).toBe(
true,
);
});
it('should fail to move files if destination already exists', async () => {
const destination = 'projects';
await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow(
'dest already exists',
);
});
it('should remove temporary files if move succeeded', async () => {
const destination = 'projects/mockApp';
await moveAppTask(tempDir, destination, id);
expect(fs.existsSync('tmp/mockApp')).toBe(false);
});
it('should remove temporary files if move failed', async () => {
const destination = 'projects';
await expect(moveAppTask(tempDir, destination, id)).rejects.toThrow();
expect(fs.existsSync('tmp/mockApp')).toBe(false);
});
});
describe('templatingTask', () => {
it('should generate a project populating context parameters', async () => {
const templateDir = 'templates/default-app';
const destinationDir = 'templatedApp';
const context = {
name: 'SuperCoolBackstageInstance',
dbTypeSqlite: true,
};
await templatingTask(templateDir, destinationDir, context);
expect(fs.existsSync('templatedApp/package.json')).toBe(true);
expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true);
await expect(fs.readJson('templatedApp/backstage.json')).resolves.toEqual(
{
version: '1.2.3',
},
);
// catalog was populated with `context.name`
expect(
fs.readFileSync('templatedApp/catalog-info.yaml', 'utf-8'),
).toContain('name: SuperCoolBackstageInstance');
// backend dependencies include `sqlite3` from `context.SQLite`
expect(
fs.readFileSync('templatedApp/packages/backend/package.json', 'utf-8'),
).toContain('sqlite3"');
});
// catalog was populated with `context.name`
expect(
fs.readFileSync('templatedApp/catalog-info.yaml', 'utf-8'),
).toContain('name: SuperCoolBackstageInstance');
// backend dependencies include `sqlite3` from `context.SQLite`
expect(
fs.readFileSync('templatedApp/packages/backend/package.json', 'utf-8'),
).toContain('sqlite3"');
});
});

Some files were not shown because too many files have changed in this diff Show More