Merge remote-tracking branch 'upstream/master' into airbrake-api

# Conflicts:
#	ADOPTERS.md
#	plugins/airbrake/package.json
This commit is contained in:
Karan Shah
2022-02-09 16:00:36 +00:00
317 changed files with 4999 additions and 1810 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': minor
---
The `callbackUrl` option of `OAuthAdapter` is now required.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/cli': patch
---
Introduces a new `--release` parameter to the `backstage-cli versions:bump` command.
The release can be either a specific version, for example `0.99.1`, or the latest `main` or `next` release.
The default behavior is to bump to the latest `main` release.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-newrelic-dashboard': patch
---
Export DashboardSnapshotComponent from new-relic-dashboard-plugin
+18
View File
@@ -0,0 +1,18 @@
---
'@backstage/backend-common': patch
'@backstage/backend-tasks': patch
'@backstage/backend-test-utils': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-auth-backend': patch
'@backstage/plugin-bazaar-backend': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-code-coverage-backend': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-search-backend-module-pg': patch
'@backstage/plugin-tech-insights-backend': patch
'@backstage/plugin-techdocs-backend': patch
---
chore(deps): bump `knex` from 0.95.6 to 1.0.2
This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Enabled refresh for the Atlassian provider.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/release-manifests': patch
---
Introduces a new package with utilities for fetching release manifests.
This package will primarily be used by the `@backstage/cli` package.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Removed unnecessary `get-port` dependency
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-splunk-on-call': patch
---
Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Adjust ErrorPage to accept optional supportUrl property to override app support config. Update type of additionalInfo property to be ReactNode to accept both string and component.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The file path printed by the default lint formatter is now relative to the repository root, rather than the individual package.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added a new `cookieConfigurer` option to `AuthProviderConfig` that makes it possible to override the default logic for configuring OAuth provider cookies.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Add userExpand option to allow users to expand fields retrieved from the Graph API - for use in custom transformers
+58
View File
@@ -0,0 +1,58 @@
---
'@backstage/create-app': patch
---
**BREAKING:** Updated `knex` to major version 1, which also implies changing out
the underlying `sqlite` implementation.
The old `sqlite3` NPM library has been abandoned by its maintainers, which has
led to unhandled security reports and other issues. Therefore, in the `knex` 1.x
release line they have instead switched over to the [`@vscode/sqlite3`
library](https://github.com/microsoft/vscode-node-sqlite3) by default, which is
actively maintained by Microsoft.
This means that as you update to this version of Backstage, there are two
breaking changes that you will have to address in your own repository:
## Bumping `knex` itself
All `package.json` files of your repo that used to depend on a 0.x version of
`knex`, should now be updated to depend on the 1.x release line. This applies in
particular to `packages/backend`, but may also occur in backend plugins or
libraries.
```diff
- "knex": "^0.95.1",
+ "knex": "^1.0.2",
```
Almost all existing database code will continue to function without modification
after this bump. The only significant difference that we discovered in the main
repo, is that the `alter()` function had a slightly different signature in
migration files. It now accepts an object with `alterType` and `alterNullable`
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
information about the `alter` syntax.
Also see the [`knex` changelog](https://knexjs.org/#changelog) for information
about breaking changes in the 1.x line; if you are using `RETURNING` you may
want to make some additional modifications in your code.
## Switching out `sqlite3`
All `package.json` files of your repo that used to depend on `sqlite3`, should
now be updated to depend on `@vscode/sqlite3`. This applies in particular to
`packages/backend`, but may also occur in backend plugins or libraries.
```diff
- "sqlite3": "^5.0.1",
+ "@vscode/sqlite3": "^5.0.7",
```
These should be functionally equivalent, except that the new library will have
addressed some long standing problems with old transitive dependencies etc.
+161
View File
@@ -0,0 +1,161 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.63",
"@backstage/app-defaults": "0.1.6",
"example-backend": "0.2.63",
"@backstage/backend-common": "0.10.6",
"@backstage/backend-tasks": "0.1.5",
"@backstage/backend-test-utils": "0.1.16",
"@backstage/catalog-client": "0.5.5",
"@backstage/catalog-model": "0.9.10",
"@backstage/cli": "0.13.1",
"@backstage/cli-common": "0.1.6",
"@backstage/codemods": "0.1.32",
"@backstage/config": "0.1.13",
"@backstage/config-loader": "0.9.3",
"@backstage/core-app-api": "0.5.2",
"@backstage/core-components": "0.8.7",
"@backstage/core-plugin-api": "0.6.0",
"@backstage/create-app": "0.4.18",
"@backstage/dev-utils": "0.2.20",
"e2e-test": "0.2.0",
"@backstage/errors": "0.2.0",
"@backstage/integration": "0.7.2",
"@backstage/integration-react": "0.1.20",
"@backstage/search-common": "0.2.2",
"@techdocs/cli": "0.8.12",
"techdocs-cli-embedded-app": "0.2.62",
"@backstage/techdocs-common": "0.11.6",
"@backstage/test-utils": "0.2.4",
"@backstage/theme": "0.2.14",
"@backstage/types": "0.1.1",
"@backstage/version-bridge": "0.1.1",
"@backstage/plugin-airbrake": "0.1.2",
"@backstage/plugin-allure": "0.1.13",
"@backstage/plugin-analytics-module-ga": "0.1.8",
"@backstage/plugin-apache-airflow": "0.1.5",
"@backstage/plugin-api-docs": "0.7.1",
"@backstage/plugin-app-backend": "0.3.23",
"@backstage/plugin-auth-backend": "0.9.0",
"@backstage/plugin-azure-devops": "0.1.13",
"@backstage/plugin-azure-devops-backend": "0.3.2",
"@backstage/plugin-azure-devops-common": "0.2.0",
"@backstage/plugin-badges": "0.2.21",
"@backstage/plugin-badges-backend": "0.1.17",
"@backstage/plugin-bazaar": "0.1.12",
"@backstage/plugin-bazaar-backend": "0.1.8",
"@backstage/plugin-bitrise": "0.1.24",
"@backstage/plugin-catalog": "0.7.11",
"@backstage/plugin-catalog-backend": "0.21.2",
"@backstage/plugin-catalog-backend-module-ldap": "0.3.11",
"@backstage/plugin-catalog-backend-module-msgraph": "0.2.14",
"@backstage/plugin-catalog-common": "0.1.2",
"@backstage/plugin-catalog-graph": "0.2.9",
"@backstage/plugin-catalog-graphql": "0.3.1",
"@backstage/plugin-catalog-import": "0.8.0",
"@backstage/plugin-catalog-react": "0.6.13",
"@backstage/plugin-circleci": "0.2.36",
"@backstage/plugin-cloudbuild": "0.2.34",
"@backstage/plugin-code-coverage": "0.1.24",
"@backstage/plugin-code-coverage-backend": "0.1.21",
"@backstage/plugin-config-schema": "0.1.20",
"@backstage/plugin-cost-insights": "0.11.19",
"@backstage/plugin-explore": "0.3.28",
"@backstage/plugin-explore-react": "0.0.11",
"@backstage/plugin-firehydrant": "0.1.14",
"@backstage/plugin-fossa": "0.2.29",
"@backstage/plugin-gcp-projects": "0.3.16",
"@backstage/plugin-git-release-manager": "0.3.10",
"@backstage/plugin-github-actions": "0.4.34",
"@backstage/plugin-github-deployments": "0.1.28",
"@backstage/plugin-gitops-profiles": "0.3.15",
"@backstage/plugin-gocd": "0.1.3",
"@backstage/plugin-graphiql": "0.2.29",
"@backstage/plugin-graphql-backend": "0.1.13",
"@backstage/plugin-home": "0.4.13",
"@backstage/plugin-ilert": "0.1.23",
"@backstage/plugin-jenkins": "0.5.19",
"@backstage/plugin-jenkins-backend": "0.1.12",
"@backstage/plugin-kafka": "0.2.27",
"@backstage/plugin-kafka-backend": "0.2.16",
"@backstage/plugin-kubernetes": "0.5.6",
"@backstage/plugin-kubernetes-backend": "0.4.6",
"@backstage/plugin-kubernetes-common": "0.2.2",
"@backstage/plugin-lighthouse": "0.2.36",
"@backstage/plugin-newrelic": "0.3.15",
"@backstage/plugin-newrelic-dashboard": "0.1.5",
"@backstage/plugin-org": "0.4.1",
"@backstage/plugin-pagerduty": "0.3.24",
"@backstage/plugin-permission-backend": "0.4.2",
"@backstage/plugin-permission-common": "0.4.0",
"@backstage/plugin-permission-node": "0.4.2",
"@backstage/plugin-permission-react": "0.3.0",
"@backstage/plugin-proxy-backend": "0.2.17",
"@backstage/plugin-rollbar": "0.3.25",
"@backstage/plugin-rollbar-backend": "0.1.20",
"@backstage/plugin-scaffolder": "0.12.1",
"@backstage/plugin-scaffolder-backend": "0.15.23",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.1.10",
"@backstage/plugin-scaffolder-backend-module-rails": "0.2.5",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.1.4",
"@backstage/plugin-scaffolder-common": "0.1.3",
"@backstage/plugin-search": "0.6.1",
"@backstage/plugin-search-backend": "0.4.1",
"@backstage/plugin-search-backend-module-elasticsearch": "0.0.8",
"@backstage/plugin-search-backend-module-pg": "0.2.5",
"@backstage/plugin-search-backend-node": "0.4.5",
"@backstage/plugin-sentry": "0.3.35",
"@backstage/plugin-shortcuts": "0.1.21",
"@backstage/plugin-sonarqube": "0.2.15",
"@backstage/plugin-splunk-on-call": "0.3.21",
"@backstage/plugin-tech-insights": "0.1.7",
"@backstage/plugin-tech-insights-backend": "0.2.3",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.7",
"@backstage/plugin-tech-insights-common": "0.2.1",
"@backstage/plugin-tech-insights-node": "0.2.1",
"@backstage/plugin-tech-radar": "0.5.4",
"@backstage/plugin-techdocs": "0.13.2",
"@backstage/plugin-techdocs-backend": "0.13.2",
"@backstage/plugin-todo": "0.1.21",
"@backstage/plugin-todo-backend": "0.1.20",
"@backstage/plugin-user-settings": "0.3.18",
"@backstage/plugin-xcmetrics": "0.2.17"
},
"changesets": [
"big-jeans-love",
"brave-tools-drop",
"breezy-windows-jump",
"chilly-pans-jog",
"cool-birds-ring",
"curly-fireants-crash",
"dependabot-e379ac7",
"dependabot-f436b5b",
"early-beds-smoke",
"healthy-flies-fold",
"khaki-jokes-grab",
"loud-monkeys-explode",
"many-terms-type",
"metal-clouds-fail",
"metal-lions-fix",
"nasty-socks-exist",
"neat-icons-fry",
"ninety-dancers-bow",
"old-phones-draw",
"popular-planes-lay",
"pretty-glasses-admire",
"seven-apes-shave",
"seven-teachers-arrive",
"shaggy-buckets-confess",
"shiny-radios-deliver",
"smart-boxes-double",
"tasty-spoons-beg",
"three-dolls-fly",
"three-pigs-sniff",
"twenty-colts-applaud",
"warm-beds-flow",
"wise-peaches-flow",
"wise-plants-tease"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Encode the `formData` in the `queryString` using `JSON.stringify` to keep the types in the decoded value
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Rather than calling `yarn pack`, the `build-workspace` and `backend-bundle` commands now move files directly whenever possible. This cuts out several `yarn` invocations and speeds the packing process up by several orders of magnitude.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages.
+5
View File
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Updated the HTTP server to allow for simplification of the development of the CLI itself.
+47 -47
View File
@@ -4,50 +4,50 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
* @backstage/reviewers
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
/.changeset/techdocs-* @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
/docs/assets/search @backstage/techdocs-core
/docs/features/search @backstage/techdocs-core
/docs/features/techdocs @backstage/techdocs-core
/packages/search-common @backstage/techdocs-core
/packages/techdocs-cli @backstage/techdocs-core
/packages/techdocs-cli-embedded-app @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps
/plugins/apache-airflow @backstage/reviewers @cmpadden
/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers
/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin
/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin
/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin
/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers
/plugins/catalog-graph @backstage/reviewers @backstage/sda-se-reviewers
/plugins/circleci @backstage/reviewers @adamdmharvey
/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios
/plugins/code-coverage @backstage/reviewers @alde @nissayeva
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
/plugins/cost-insights @backstage/silver-lining
/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers
/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers
/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers
/plugins/git-release-manager @backstage/reviewers @erikengervall
/plugins/home @backstage/techdocs-core
/plugins/ilert @backstage/reviewers @yacut
/plugins/jenkins @backstage/reviewers @timja
/plugins/jenkins-backend @backstage/reviewers @timja
/plugins/kafka @backstage/reviewers @nirga
/plugins/kafka-backend @backstage/reviewers @nirga
/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7
/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski
/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka
/plugins/search @backstage/techdocs-core
/plugins/search-* @backstage/techdocs-core
/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers
/plugins/techdocs @backstage/techdocs-core
/plugins/techdocs-backend @backstage/techdocs-core
/tech-insights-backend @backstage/reviewers @xantier @iain-b
/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b
* @backstage/reviewers
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/reviewers @backstage/techdocs-core
/.changeset/techdocs-* @backstage/reviewers @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/reviewers @backstage/techdocs-core
/docs/assets/search @backstage/reviewers @backstage/techdocs-core
/docs/features/search @backstage/reviewers @backstage/techdocs-core
/docs/features/techdocs @backstage/reviewers @backstage/techdocs-core
/packages/search-common @backstage/reviewers @backstage/techdocs-core
/packages/techdocs-cli @backstage/reviewers @backstage/techdocs-core
/packages/techdocs-cli-embedded-app @backstage/reviewers @backstage/techdocs-core
/packages/techdocs-common @backstage/reviewers @backstage/techdocs-core
/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps
/plugins/apache-airflow @backstage/reviewers @cmpadden
/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers
/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin
/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin
/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin
/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers
/plugins/catalog-graph @backstage/reviewers @backstage/sda-se-reviewers
/plugins/circleci @backstage/reviewers @adamdmharvey
/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios
/plugins/code-coverage @backstage/reviewers @alde @nissayeva
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
/plugins/cost-insights @backstage/reviewers @backstage/silver-lining
/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers
/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers
/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers
/plugins/git-release-manager @backstage/reviewers @erikengervall
/plugins/home @backstage/reviewers @backstage/techdocs-core
/plugins/ilert @backstage/reviewers @yacut
/plugins/jenkins @backstage/reviewers @timja
/plugins/jenkins-backend @backstage/reviewers @timja
/plugins/kafka @backstage/reviewers @nirga
/plugins/kafka-backend @backstage/reviewers @nirga
/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7
/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski
/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka
/plugins/search @backstage/reviewers @backstage/techdocs-core
/plugins/search-* @backstage/reviewers @backstage/techdocs-core
/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers
/plugins/techdocs @backstage/reviewers @backstage/techdocs-core
/plugins/techdocs-backend @backstage/reviewers @backstage/techdocs-core
/tech-insights-backend @backstage/reviewers @xantier @iain-b
/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b
/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b
+11 -12
View File
@@ -4,20 +4,17 @@ about: 'Create Bug Report'
labels: bug
---
<!--- Provide a general summary of the issue in the Title above -->
<!---
Please use this template when reporting bugs. Thank you!
-->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
## Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas as to the implementation of the addition or change -->
<!--- Tell us what happens instead -->
## Steps to Reproduce
@@ -38,8 +35,10 @@ labels: bug
<!--- Include as many relevant details about the environment you experienced the bug in -->
<!-- ProTip: You can use `yarn backstage-cli info` command in your Backstage App for this section. -->
- Browser Information: <!--- For example Google Chrome 97.0.4692.99 -->
- NodeJS Version (v14):
- Operating System and Version (e.g. Ubuntu 14.04):
- Browser Information:
- Output of `yarn backstage-cli info`: <!--- Paste into the code block below -->
```text
```
+12
View File
@@ -0,0 +1,12 @@
---
blank_issues_enabled: false
contact_links:
- about: 'Please ask and answer usage questions in GitHub Discussions'
name: Question
url: 'https://github.com/backstage/backstage/discussions'
- about: 'Alternatively, you can use the Backstage Community Discord'
name: Chat
url: 'https://discord.gg/MUpMjP2'
- about: 'Please check the FAQ before filing new issues'
name: 'Backstage FAQ'
url: 'https://backstage.io/docs/FAQ'
+1 -1
View File
@@ -205,7 +205,7 @@ jobs:
# Grabs the version in the root package.json and creates a tag on GitHub
- name: Create a release tag
id: create_tag
run: node scripts/create-release-tag.js
run: node scripts/create-release-tag.js --dispatch-workflows
env:
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
@@ -48,3 +48,31 @@ jobs:
git add .
git commit -am "${{ github.event.inputs.version }}"
git push
- name: Dispatch update-helper update
uses: actions/github-script@v5
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
script: |
console.log('Dispatching upgrade helper sync - release version');
await github.rest.actions.createWorkflowDispatch({
owner: 'backstage',
repo: 'upgrade-helper-diff',
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./backstage/package.json').version,
},
});
console.log('Dispatching upgrade helper sync - create-app version');
await github.rest.actions.createWorkflowDispatch({
owner: 'backstage',
repo: 'upgrade-helper-diff',
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./backstage/packages/create-app/package.json').version,
},
});
+65
View File
@@ -0,0 +1,65 @@
name: Verify DCO
on:
schedule:
- cron: '*/15 * * * *'
jobs:
dco-helper:
runs-on: ubuntu-latest
steps:
- name: Verify DCO status for open pull requests
uses: actions/github-script@v5
with:
script: |
const owner = "backstage";
const repo = "backstage";
const pulls = await github.paginate(github.rest.pulls.list, {
state: "open",
owner,
repo,
});
for (const pull of pulls) {
// Pick out the PRs that have the DCO check
const checks = await github.rest.checks.listForRef({
owner,
repo,
ref: pull.head.sha,
check_name: "DCO",
status: "completed",
});
// Skip if there are no checks
if (!checks.data.check_runs.length) {
continue;
}
// Skip if the conclusion is not action_required
if (checks.data.check_runs[0].conclusion !== "action_required") {
console.log(`No checks found for PR #${pull.number}, skipping`);
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pull.number,
});
if (comments.find((c) =>
c.user.login === "github-actions[bot]" &&
c.body.includes("<!-- dco -->")
)
) {
console.log(`already commented on PR #${pull.number}, skipping`);
continue;
}
console.log(`creating comment on PR #${pull.number}`);
const body = `
Thanks for the contribution!
All commits need to be DCO signed before merging. Please refer to the the [DCO section in CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#developer-certificate-of-origin) or the [DCO](${checks.data.check_runs[0].html_url}) status for more info.
<!-- dco -->`;
await github.rest.issues.createComment({
repo,
owner,
issue_number: pull.number,
body,
});
}
+3 -1
View File
@@ -71,7 +71,7 @@
| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! |
| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. |
| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 |
| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates<br/> Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc |
| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. |
| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 |
@@ -90,3 +90,5 @@
| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. |
| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. |
| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. |
@@ -114,7 +114,7 @@ updated documentation next time you run Backstage!
## Writing and previewing your documentation
Using the [techdocs-cli](https://github.com/backstage/techdocs-cli) you can
Using the [techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli) you can
preview your docs inside a local Backstage instance and get live reload on
changes. This is useful when you want to preview your documentation while
writing.
+1 -1
View File
@@ -49,7 +49,7 @@
"**/@roadiehq/**/@backstage/plugin-catalog": "*",
"**/@roadiehq/**/@backstage/catalog-model": "*"
},
"version": "0.66.0",
"version": "0.67.0-next.0",
"dependencies": {
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.15.0",
+7
View File
@@ -1,5 +1,12 @@
# @backstage/app-defaults
## 0.1.7-next.0
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.8-next.0
## 0.1.6
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
"version": "0.1.6",
"version": "0.1.7-next.0",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-app-api": "^0.5.2",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/plugin-permission-react": "^0.3.0",
@@ -42,7 +42,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+47
View File
@@ -1,5 +1,52 @@
# example-app
## 0.2.64-next.0
### Patch Changes
- Updated dependencies
- @backstage/cli@0.13.2-next.0
- @backstage/plugin-todo@0.2.0-next.0
- @backstage/plugin-newrelic-dashboard@0.1.6-next.0
- @backstage/core-components@0.8.8-next.0
- @backstage/plugin-scaffolder@0.12.2-next.0
- @backstage/plugin-search@0.6.2-next.0
- @backstage/plugin-catalog-react@0.6.14-next.0
- @backstage/plugin-catalog-graph@0.2.10-next.0
- @backstage/plugin-catalog-import@0.8.1-next.0
- @backstage/plugin-home@0.4.14-next.0
- @backstage/app-defaults@0.1.7-next.0
- @backstage/integration-react@0.1.21-next.0
- @backstage/plugin-airbrake@0.1.3-next.0
- @backstage/plugin-apache-airflow@0.1.6-next.0
- @backstage/plugin-api-docs@0.7.2-next.0
- @backstage/plugin-azure-devops@0.1.14-next.0
- @backstage/plugin-badges@0.2.22-next.0
- @backstage/plugin-catalog@0.7.12-next.0
- @backstage/plugin-circleci@0.2.37-next.0
- @backstage/plugin-cloudbuild@0.2.35-next.0
- @backstage/plugin-code-coverage@0.1.25-next.0
- @backstage/plugin-cost-insights@0.11.20-next.0
- @backstage/plugin-explore@0.3.29-next.0
- @backstage/plugin-gcp-projects@0.3.17-next.0
- @backstage/plugin-github-actions@0.4.35-next.0
- @backstage/plugin-gocd@0.1.4-next.0
- @backstage/plugin-graphiql@0.2.30-next.0
- @backstage/plugin-jenkins@0.5.20-next.0
- @backstage/plugin-kafka@0.2.28-next.0
- @backstage/plugin-kubernetes@0.5.7-next.0
- @backstage/plugin-lighthouse@0.2.37-next.0
- @backstage/plugin-newrelic@0.3.16-next.0
- @backstage/plugin-org@0.4.2-next.0
- @backstage/plugin-pagerduty@0.3.25-next.0
- @backstage/plugin-rollbar@0.3.26-next.0
- @backstage/plugin-sentry@0.3.36-next.0
- @backstage/plugin-shortcuts@0.1.22-next.0
- @backstage/plugin-tech-insights@0.1.8-next.0
- @backstage/plugin-tech-radar@0.5.5-next.0
- @backstage/plugin-techdocs@0.13.3-next.0
- @backstage/plugin-user-settings@0.3.19-next.0
## 0.2.63
### Patch Changes
+42 -42
View File
@@ -1,56 +1,56 @@
{
"name": "example-app",
"version": "0.2.63",
"version": "0.2.64-next.0",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.6",
"@backstage/app-defaults": "^0.1.7-next.0",
"@backstage/catalog-model": "^0.9.10",
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/core-app-api": "^0.5.2",
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/integration-react": "^0.1.20",
"@backstage/plugin-airbrake": "^0.1.2",
"@backstage/plugin-api-docs": "^0.7.1",
"@backstage/plugin-azure-devops": "^0.1.13",
"@backstage/plugin-apache-airflow": "^0.1.5",
"@backstage/plugin-badges": "^0.2.21",
"@backstage/plugin-catalog": "^0.7.11",
"@backstage/integration-react": "^0.1.21-next.0",
"@backstage/plugin-airbrake": "^0.1.3-next.0",
"@backstage/plugin-api-docs": "^0.7.2-next.0",
"@backstage/plugin-azure-devops": "^0.1.14-next.0",
"@backstage/plugin-apache-airflow": "^0.1.6-next.0",
"@backstage/plugin-badges": "^0.2.22-next.0",
"@backstage/plugin-catalog": "^0.7.12-next.0",
"@backstage/plugin-catalog-common": "^0.1.2",
"@backstage/plugin-catalog-graph": "^0.2.9",
"@backstage/plugin-catalog-import": "^0.8.0",
"@backstage/plugin-catalog-react": "^0.6.13",
"@backstage/plugin-circleci": "^0.2.36",
"@backstage/plugin-cloudbuild": "^0.2.34",
"@backstage/plugin-code-coverage": "^0.1.24",
"@backstage/plugin-cost-insights": "^0.11.19",
"@backstage/plugin-explore": "^0.3.28",
"@backstage/plugin-gcp-projects": "^0.3.16",
"@backstage/plugin-github-actions": "^0.4.34",
"@backstage/plugin-gocd": "^0.1.3",
"@backstage/plugin-graphiql": "^0.2.29",
"@backstage/plugin-home": "^0.4.13",
"@backstage/plugin-jenkins": "^0.5.19",
"@backstage/plugin-kafka": "^0.2.27",
"@backstage/plugin-kubernetes": "^0.5.6",
"@backstage/plugin-lighthouse": "^0.2.36",
"@backstage/plugin-newrelic": "^0.3.15",
"@backstage/plugin-newrelic-dashboard": "^0.1.5",
"@backstage/plugin-org": "^0.4.1",
"@backstage/plugin-pagerduty": "0.3.24",
"@backstage/plugin-catalog-graph": "^0.2.10-next.0",
"@backstage/plugin-catalog-import": "^0.8.1-next.0",
"@backstage/plugin-catalog-react": "^0.6.14-next.0",
"@backstage/plugin-circleci": "^0.2.37-next.0",
"@backstage/plugin-cloudbuild": "^0.2.35-next.0",
"@backstage/plugin-code-coverage": "^0.1.25-next.0",
"@backstage/plugin-cost-insights": "^0.11.20-next.0",
"@backstage/plugin-explore": "^0.3.29-next.0",
"@backstage/plugin-gcp-projects": "^0.3.17-next.0",
"@backstage/plugin-github-actions": "^0.4.35-next.0",
"@backstage/plugin-gocd": "^0.1.4-next.0",
"@backstage/plugin-graphiql": "^0.2.30-next.0",
"@backstage/plugin-home": "^0.4.14-next.0",
"@backstage/plugin-jenkins": "^0.5.20-next.0",
"@backstage/plugin-kafka": "^0.2.28-next.0",
"@backstage/plugin-kubernetes": "^0.5.7-next.0",
"@backstage/plugin-lighthouse": "^0.2.37-next.0",
"@backstage/plugin-newrelic": "^0.3.16-next.0",
"@backstage/plugin-newrelic-dashboard": "^0.1.6-next.0",
"@backstage/plugin-org": "^0.4.2-next.0",
"@backstage/plugin-pagerduty": "0.3.25-next.0",
"@backstage/plugin-permission-react": "^0.3.0",
"@backstage/plugin-rollbar": "^0.3.25",
"@backstage/plugin-scaffolder": "^0.12.1",
"@backstage/plugin-search": "^0.6.1",
"@backstage/plugin-sentry": "^0.3.35",
"@backstage/plugin-shortcuts": "^0.1.21",
"@backstage/plugin-tech-radar": "^0.5.4",
"@backstage/plugin-techdocs": "^0.13.2",
"@backstage/plugin-todo": "^0.1.21",
"@backstage/plugin-user-settings": "^0.3.18",
"@backstage/plugin-rollbar": "^0.3.26-next.0",
"@backstage/plugin-scaffolder": "^0.12.2-next.0",
"@backstage/plugin-search": "^0.6.2-next.0",
"@backstage/plugin-sentry": "^0.3.36-next.0",
"@backstage/plugin-shortcuts": "^0.1.22-next.0",
"@backstage/plugin-tech-radar": "^0.5.5-next.0",
"@backstage/plugin-techdocs": "^0.13.3-next.0",
"@backstage/plugin-todo": "^0.2.0-next.0",
"@backstage/plugin-user-settings": "^0.3.19-next.0",
"@backstage/search-common": "^0.2.2",
"@backstage/plugin-tech-insights": "^0.1.7",
"@backstage/plugin-tech-insights": "^0.1.8-next.0",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/backend-common
## 0.10.7-next.0
### Patch Changes
- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2
This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7
- 599f3dfa83: chore(deps-dev): bump `@types/concat-stream` from 1.6.1 to 2.0.0
- c3868458d8: Removed unnecessary `get-port` dependency
## 0.10.6
### Patch Changes
+1 -1
View File
@@ -177,7 +177,7 @@ export const createDatabase: typeof createDatabaseClient;
export function createDatabaseClient(
dbConfig: Config,
overrides?: Partial<Knex.Config>,
): Knex<any, unknown[]>;
): Knex<any, Record<string, any>[]>;
// @public
export function createRootLogger(
+3 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.10.6",
"version": "0.10.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -58,7 +58,7 @@
"jose": "^1.27.1",
"keyv": "^4.0.3",
"keyv-memcache": "^1.2.5",
"knex": "^0.95.1",
"knex": "^1.0.2",
"lodash": "^4.17.21",
"logform": "^2.3.2",
"luxon": "^2.0.2",
@@ -84,7 +84,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/test-utils": "^0.2.4",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
@@ -101,7 +101,6 @@
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"aws-sdk-mock": "^5.2.1",
"get-port": "^5.1.1",
"http-errors": "^2.0.0",
"jest": "^26.0.1",
"mock-fs": "^5.1.0",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/backend-tasks
## 0.1.6-next.0
### Patch Changes
- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2
This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7
- Updated dependencies
- @backstage/backend-common@0.10.7-next.0
## 0.1.5
### Patch Changes
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
"version": "0.1.5",
"version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,12 +29,12 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.6",
"@backstage/backend-common": "^0.10.7-next.0",
"@backstage/config": "^0.1.13",
"@backstage/errors": "^0.2.0",
"@backstage/types": "^0.1.1",
"@types/luxon": "^2.0.4",
"knex": "^0.95.1",
"knex": "^1.0.2",
"lodash": "^4.17.21",
"luxon": "^2.0.2",
"node-abort-controller": "^3.0.1",
@@ -43,8 +43,8 @@
"zod": "^3.9.5"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.1.16",
"@backstage/cli": "^0.13.1",
"@backstage/backend-test-utils": "^0.1.17-next.0",
"@backstage/cli": "^0.13.2-next.0",
"jest": "^26.0.1",
"wait-for-expect": "^3.0.2"
},
+12
View File
@@ -1,5 +1,17 @@
# @backstage/backend-test-utils
## 0.1.17-next.0
### Patch Changes
- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2
This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7
- Updated dependencies
- @backstage/cli@0.13.2-next.0
- @backstage/backend-common@0.10.7-next.0
## 0.1.16
### Patch Changes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
"version": "0.1.16",
"version": "0.1.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,18 +30,18 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.10.6",
"@backstage/cli": "^0.13.1",
"@backstage/backend-common": "^0.10.7-next.0",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/config": "^0.1.13",
"knex": "^0.95.1",
"@vscode/sqlite3": "^5.0.7",
"knex": "^1.0.2",
"mysql2": "^2.2.5",
"pg": "^8.3.0",
"sqlite3": "^5.0.1",
"testcontainers": "^8.1.2",
"uuid": "^8.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"jest": "^26.0.1"
},
"files": [
+32
View File
@@ -1,5 +1,37 @@
# example-backend
## 0.2.64-next.0
### Patch Changes
- Updated dependencies
- @backstage/plugin-auth-backend@0.10.0-next.0
- @backstage/backend-common@0.10.7-next.0
- @backstage/backend-tasks@0.1.6-next.0
- @backstage/plugin-app-backend@0.3.24-next.0
- @backstage/plugin-catalog-backend@0.21.3-next.0
- @backstage/plugin-code-coverage-backend@0.1.22-next.0
- @backstage/plugin-scaffolder-backend@0.15.24-next.0
- @backstage/plugin-search-backend-module-pg@0.2.6-next.0
- @backstage/plugin-tech-insights-backend@0.2.4-next.0
- @backstage/plugin-techdocs-backend@0.13.3-next.0
- example-app@0.2.64-next.0
- @backstage/plugin-azure-devops-backend@0.3.3-next.0
- @backstage/plugin-badges-backend@0.1.18-next.0
- @backstage/plugin-graphql-backend@0.1.14-next.0
- @backstage/plugin-jenkins-backend@0.1.13-next.0
- @backstage/plugin-kafka-backend@0.2.17-next.0
- @backstage/plugin-kubernetes-backend@0.4.7-next.0
- @backstage/plugin-permission-backend@0.4.3-next.0
- @backstage/plugin-permission-node@0.4.3-next.0
- @backstage/plugin-proxy-backend@0.2.18-next.0
- @backstage/plugin-rollbar-backend@0.1.21-next.0
- @backstage/plugin-scaffolder-backend-module-rails@0.2.6-next.0
- @backstage/plugin-search-backend@0.4.2-next.0
- @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.8-next.0
- @backstage/plugin-tech-insights-node@0.2.2-next.0
- @backstage/plugin-todo-backend@0.1.21-next.0
## 0.2.63
### Patch Changes
+29 -29
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.63",
"version": "0.2.64-next.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,55 +24,55 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.10.6",
"@backstage/backend-tasks": "^0.1.5",
"@backstage/backend-common": "^0.10.7-next.0",
"@backstage/backend-tasks": "^0.1.6-next.0",
"@backstage/catalog-client": "^0.5.5",
"@backstage/catalog-model": "^0.9.10",
"@backstage/config": "^0.1.13",
"@backstage/integration": "^0.7.2",
"@backstage/plugin-app-backend": "^0.3.23",
"@backstage/plugin-auth-backend": "^0.9.0",
"@backstage/plugin-azure-devops-backend": "^0.3.2",
"@backstage/plugin-badges-backend": "^0.1.17",
"@backstage/plugin-catalog-backend": "^0.21.2",
"@backstage/plugin-code-coverage-backend": "^0.1.21",
"@backstage/plugin-graphql-backend": "^0.1.13",
"@backstage/plugin-jenkins-backend": "^0.1.12",
"@backstage/plugin-kubernetes-backend": "^0.4.6",
"@backstage/plugin-kafka-backend": "^0.2.16",
"@backstage/plugin-permission-backend": "^0.4.2",
"@backstage/plugin-app-backend": "^0.3.24-next.0",
"@backstage/plugin-auth-backend": "^0.10.0-next.0",
"@backstage/plugin-azure-devops-backend": "^0.3.3-next.0",
"@backstage/plugin-badges-backend": "^0.1.18-next.0",
"@backstage/plugin-catalog-backend": "^0.21.3-next.0",
"@backstage/plugin-code-coverage-backend": "^0.1.22-next.0",
"@backstage/plugin-graphql-backend": "^0.1.14-next.0",
"@backstage/plugin-jenkins-backend": "^0.1.13-next.0",
"@backstage/plugin-kubernetes-backend": "^0.4.7-next.0",
"@backstage/plugin-kafka-backend": "^0.2.17-next.0",
"@backstage/plugin-permission-backend": "^0.4.3-next.0",
"@backstage/plugin-permission-common": "^0.4.0",
"@backstage/plugin-permission-node": "^0.4.2",
"@backstage/plugin-proxy-backend": "^0.2.17",
"@backstage/plugin-rollbar-backend": "^0.1.20",
"@backstage/plugin-scaffolder-backend": "^0.15.23",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.5",
"@backstage/plugin-search-backend": "^0.4.1",
"@backstage/plugin-permission-node": "^0.4.3-next.0",
"@backstage/plugin-proxy-backend": "^0.2.18-next.0",
"@backstage/plugin-rollbar-backend": "^0.1.21-next.0",
"@backstage/plugin-scaffolder-backend": "^0.15.24-next.0",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.2.6-next.0",
"@backstage/plugin-search-backend": "^0.4.2-next.0",
"@backstage/plugin-search-backend-node": "^0.4.5",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.8",
"@backstage/plugin-search-backend-module-pg": "^0.2.5",
"@backstage/plugin-techdocs-backend": "^0.13.2",
"@backstage/plugin-tech-insights-backend": "^0.2.3",
"@backstage/plugin-tech-insights-node": "^0.2.1",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.7",
"@backstage/plugin-todo-backend": "^0.1.20",
"@backstage/plugin-search-backend-module-pg": "^0.2.6-next.0",
"@backstage/plugin-techdocs-backend": "^0.13.3-next.0",
"@backstage/plugin-tech-insights-backend": "^0.2.4-next.0",
"@backstage/plugin-tech-insights-node": "^0.2.2-next.0",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.8-next.0",
"@backstage/plugin-todo-backend": "^0.1.21-next.0",
"@gitbeaker/node": "^35.1.0",
"@octokit/rest": "^18.5.3",
"@vscode/sqlite3": "^5.0.7",
"azure-devops-node-api": "^11.0.1",
"dockerode": "^3.3.1",
"example-app": "link:../app",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-prom-bundle": "^6.3.6",
"knex": "^0.95.1",
"knex": "^1.0.2",
"pg": "^8.3.0",
"pg-connection-string": "^2.3.0",
"prom-client": "^14.0.1",
"sqlite3": "^5.0.1",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5"
+1 -1
View File
@@ -35,7 +35,7 @@
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/cli": "^0.13.2-next.0",
"@types/jest": "^26.0.7",
"msw": "^0.35.0"
},
+1 -1
View File
@@ -43,7 +43,7 @@
"yup": "^0.32.9"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/cli": "^0.13.2-next.0",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
+19
View File
@@ -1,5 +1,24 @@
# @backstage/cli
## 0.13.2-next.0
### Patch Changes
- bbbaa8ed61: The `plugin:diff` command no longer validates the existence of any of the files within `dev/` or `src/`.
- eaf67f0578: Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet.
- d59b90852a: The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread.
- 50a19ff8dd: The file path printed by the default lint formatter is now relative to the repository root, rather than the individual package.
- 63181dee79: Tweaked frontend bundling configuration to avoid leaking declarations into global scope.
- fae2aee878: Removed the `import/no-duplicates` lint rule from the frontend and backend ESLint configurations. This rule is quite expensive to execute and only provides a purely cosmetic benefit, so we opted to remove it from the set of default rules. If you would like to keep this rule you can add it back in your local ESLint configuration:
```js
'import/no-duplicates': 'warn'
```
- b906f98119: Rather than calling `yarn pack`, the `build-workspace` and `backend-bundle` commands now move files directly whenever possible. This cuts out several `yarn` invocations and speeds the packing process up by several orders of magnitude.
- d0c71e2aa4: Switched the `lint` command to invoke ESLint directly through its Node.js API rather than spawning a new process.
- d59b90852a: Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages.
## 0.13.1
### Patch Changes
+9 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.13.1",
"version": "0.13.2-next.0",
"private": false,
"publishConfig": {
"access": "public"
@@ -32,6 +32,7 @@
"@backstage/config": "^0.1.13",
"@backstage/config-loader": "^0.9.3",
"@backstage/errors": "^0.2.0",
"@backstage/release-manifests": "^0.0.0",
"@backstage/types": "^0.1.1",
"@hot-loader/react-dom": "^16.13.0",
"@manypkg/get-packages": "^1.1.3",
@@ -86,6 +87,7 @@
"lodash": "^4.17.21",
"minimatch": "3.0.4",
"mini-css-extract-plugin": "^2.4.2",
"npm-packlist": "^3.0.0",
"node-libs-browser": "^2.2.1",
"ora": "^5.3.0",
"postcss": "^8.1.0",
@@ -116,12 +118,12 @@
"zod": "^3.11.6"
},
"devDependencies": {
"@backstage/backend-common": "^0.10.6",
"@backstage/backend-common": "^0.10.7-next.0",
"@backstage/config": "^0.1.13",
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/core-app-api": "^0.5.2",
"@backstage/dev-utils": "^0.2.20",
"@backstage/dev-utils": "^0.2.21-next.0",
"@backstage/test-utils": "^0.2.4",
"@backstage/theme": "^0.2.14",
"@types/diff": "^5.0.0",
@@ -132,6 +134,7 @@
"@types/minimatch": "^3.0.5",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/npm-packlist": "^1.1.2",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
"@types/rollup-plugin-postcss": "^2.0.0",
@@ -141,7 +144,8 @@
"del": "^6.0.0",
"mock-fs": "^5.1.0",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
"ts-node": "^10.0.0",
"msw": "^0.35.0"
},
"peerDependencies": {
"@microsoft/api-extractor": "^7.19.2"
+2 -2
View File
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import { Command } from 'commander';
import { buildBundle } from '../../lib/bundler';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
@@ -25,7 +25,7 @@ export default async (cmd: Command) => {
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
await buildBundle({
entry: 'src/index',
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: cmd.stats,
...(await loadCliConfig({
args: cmd.config,
+2 -2
View File
@@ -21,7 +21,7 @@ import tar, { CreateOptions } from 'tar';
import { Command } from 'commander';
import { createDistWorkspace } from '../../lib/packager';
import { paths } from '../../lib/paths';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { buildPackage, Output } from '../../lib/builder';
const BUNDLE_FILE = 'bundle.tar.gz';
@@ -40,7 +40,7 @@ export default async (cmd: Command) => {
targetDir: tmpDir,
buildDependencies: Boolean(cmd.buildDependencies),
buildExcludes: [pkg.name],
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
skeleton: SKELETON_FILE,
});
+9 -6
View File
@@ -15,24 +15,27 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { buildBundle } from '../../lib/bundler';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
interface BuildAppOptions {
targetDir: string;
writeStats: boolean;
configPaths: string[];
}
export async function buildApp(options: BuildAppOptions) {
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
const { targetDir, writeStats, configPaths } = options;
const { name } = await fs.readJson(resolvePath(targetDir, 'package.json'));
await buildBundle({
targetDir,
entry: 'src/index',
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
statsJsonEnabled: options.writeStats,
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: writeStats,
...(await loadCliConfig({
args: options.configPaths,
args: configPaths,
fromPackage: name,
})),
});
@@ -19,20 +19,20 @@ import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import tar, { CreateOptions } from 'tar';
import { createDistWorkspace } from '../../lib/packager';
import { paths } from '../../lib/paths';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { buildPackage, Output } from '../../lib/builder';
const BUNDLE_FILE = 'bundle.tar.gz';
const SKELETON_FILE = 'skeleton.tar.gz';
interface BuildBackendOptions {
targetDir: string;
skipBuildDependencies: boolean;
}
export async function buildBackend(options: BuildBackendOptions) {
const targetDir = paths.resolveTarget('dist');
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
const { targetDir, skipBuildDependencies } = options;
const pkg = await fs.readJson(resolvePath(targetDir, 'package.json'));
// We build the target package without generating type declarations.
await buildPackage({ outputs: new Set([Output.cjs]) });
@@ -41,9 +41,9 @@ export async function buildBackend(options: BuildBackendOptions) {
try {
await createDistWorkspace([pkg.name], {
targetDir: tmpDir,
buildDependencies: !options.skipBuildDependencies,
buildDependencies: !skipBuildDependencies,
buildExcludes: [pkg.name],
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
skeleton: SKELETON_FILE,
});
@@ -17,6 +17,7 @@
import { Command } from 'commander';
import { buildPackage, Output } from '../../lib/builder';
import { findRoleFromCommand, getRoleInfo } from '../../lib/role';
import { paths } from '../../lib/paths';
import { buildApp } from './buildApp';
import { buildBackend } from './buildBackend';
@@ -25,12 +26,14 @@ export async function command(cmd: Command): Promise<void> {
if (role === 'app') {
return buildApp({
targetDir: paths.resolveTarget('dist'),
configPaths: cmd.config as string[],
writeStats: Boolean(cmd.stats),
});
}
if (role === 'backend') {
return buildBackend({
targetDir: paths.resolveTarget('dist'),
skipBuildDependencies: Boolean(cmd.skipBuildDependencies),
});
}
+25
View File
@@ -25,6 +25,25 @@ const configOption = [
Array<string>(),
] as const;
export function registerRepoCommand(program: CommanderStatic) {
const command = program
.command('repo [command]', { hidden: true })
.description(
'Command that run across an entire Backstage project [EXPERIMENTAL]',
);
command
.command('build')
.description(
'Build packages in the project, excluding bundled app and backend packages.',
)
.option(
'--all',
'Build all packages, including bundled app and backend packages.',
)
.action(lazy(() => import('./repo/build').then(m => m.command)));
}
export function registerScriptCommand(program: CommanderStatic) {
const command = program
.command('script [command]', { hidden: true })
@@ -312,6 +331,7 @@ export function registerCommands(program: CommanderStatic) {
.description('Print configuration schema')
.action(lazy(() => import('./config/schema').then(m => m.default)));
registerRepoCommand(program);
registerScriptCommand(program);
registerMigrateCommand(program);
@@ -321,6 +341,11 @@ export function registerCommands(program: CommanderStatic) {
'--pattern <glob>',
'Override glob for matching packages to upgrade',
)
.option(
'--release <version|next|main>',
'Bump to a specific Backstage release line or version',
'main',
)
.description('Bump Backstage packages to the latest versions')
.action(lazy(() => import('./versions/bump').then(m => m.default)));
+7 -2
View File
@@ -18,20 +18,25 @@ import { Command } from 'commander';
import { paths } from '../lib/paths';
import { ESLint } from 'eslint';
export default async (cmd: Command) => {
export default async (cmd: Command, cmdArgs: string[]) => {
const eslint = new ESLint({
cwd: paths.targetDir,
fix: cmd.fix,
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
});
const results = await eslint.lintFiles(['.']);
const results = await eslint.lintFiles(cmdArgs ?? ['.']);
if (cmd.fix) {
await ESLint.outputFixes(results);
}
const formatter = await eslint.loadFormatter(cmd.format);
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
if (cmd.format === 'eslint-formatter-friendly') {
process.chdir(paths.targetRoot);
}
const resultText = formatter.format(results);
// If there is any feedback at all, we treat it as a lint failure. This should be
+167
View File
@@ -0,0 +1,167 @@
/*
* 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 chalk from 'chalk';
import { Command } from 'commander';
import { relative as relativePath } from 'path';
import { buildPackages, getOutputsForRole } from '../../lib/builder';
import { PackageGraph } from '../../lib/monorepo';
import { ExtendedPackage } from '../../lib/monorepo/PackageGraph';
import { runParallelWorkers } from '../../lib/parallel';
import { paths } from '../../lib/paths';
import { detectRoleFromPackage } from '../../lib/role';
import { buildApp } from '../build/buildApp';
import { buildBackend } from '../build/buildBackend';
function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) {
// Regardless of what command instance is passed in we want to find
// the root command and resolve the path from there
let rootCmd = anyCmd;
while (rootCmd.parent) {
rootCmd = rootCmd.parent;
}
// Now find the command that was requested
let targetCmd = rootCmd as Command | undefined;
for (const name of commandPath) {
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
| Command
| undefined;
}
if (!targetCmd) {
throw new Error(`Could not find script command '${commandPath.join(' ')}'`);
}
const cmd = targetCmd;
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
return (scriptStr?: string) => {
if (!scriptStr || !scriptStr.startsWith(expectedScript)) {
return undefined;
}
const argsStr = scriptStr.slice(expectedScript.length).trim();
// Can't clone or copy or even use commands as prototype, so we mutate
// the necessary members instead, and then reset them once we're done
const currentOpts = cmd._optionValues;
const currentStore = cmd._storeOptionsAsProperties;
const result: Record<string, any> = {};
cmd._storeOptionsAsProperties = false;
cmd._optionValues = result;
// Triggers the writing of options to the result object
cmd.parseOptions(argsStr.split(' '));
cmd._storeOptionsAsProperties = currentOpts;
cmd._optionValues = currentStore;
return result;
};
}
export async function command(cmd: Command): Promise<void> {
const packages = await PackageGraph.listTargetPackages();
const apps = new Array<ExtendedPackage>();
const backends = new Array<ExtendedPackage>();
const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']);
const options = packages.flatMap(pkg => {
const role =
pkg.packageJson.backstage?.role ?? detectRoleFromPackage(pkg.packageJson);
if (!role) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no role`);
return [];
}
if (role === 'app') {
apps.push(pkg);
return [];
} else if (role === 'backend') {
backends.push(pkg);
return [];
}
const outputs = getOutputsForRole(role);
if (outputs.size === 0) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no output`);
return [];
}
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return [];
}
return {
targetDir: pkg.dir,
outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
minify: buildOptions.minify,
useApiExtractor: buildOptions.experimentalTypeBuild,
};
});
console.log('Building packages');
await buildPackages(options);
if (cmd.all) {
console.log('Building apps');
await runParallelWorkers({
items: apps,
parallelismFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return;
}
await buildApp({
targetDir: pkg.dir,
configPaths: (buildOptions.config as string[]) ?? [],
writeStats: Boolean(buildOptions.stats),
});
},
});
console.log('Building backends');
await runParallelWorkers({
items: backends,
parallelismFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return;
}
await buildBackend({
targetDir: pkg.dir,
skipBuildDependencies: true,
});
},
});
}
}
+382 -54
View File
@@ -20,8 +20,14 @@ import { Command } from 'commander';
import { resolve as resolvePath } from 'path';
import { paths } from '../../lib/paths';
import * as runObj from '../../lib/run';
import bump, { bumpBackstageJsonVersion } from './bump';
import { withLogCollector } from '@backstage/test-utils';
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
import {
setupRequestMockHandlers,
withLogCollector,
} from '@backstage/test-utils';
import { YarnInfoInspectData } from '../../lib/versioning/packages';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
// Remove log coloring to simplify log matching
jest.mock('chalk', () => ({
@@ -38,6 +44,7 @@ const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/theme': '2.0.0',
'@backstage-extra/custom': '1.1.0',
'@backstage-extra/custom-two': '2.0.0',
'@backstage/create-app': '1.0.0',
};
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
@@ -82,6 +89,8 @@ describe('bump', () => {
mockFs.restore();
jest.resetAllMocks();
});
const worker = setupServer();
setupRequestMockHandlers(worker);
it('should bump backstage dependencies', async () => {
mockFs({
@@ -121,14 +130,25 @@ describe('bump', () => {
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: null } as unknown as Command);
await bump({ pattern: null, release: 'main' } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
@@ -142,7 +162,7 @@ describe('bump', () => {
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(4);
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
@@ -179,6 +199,225 @@ describe('bump', () => {
});
});
it('should prefer dependency versions from release manifest', async () => {
mockFs({
'/yarn.lock': lockfileMock,
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
},
}),
});
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
releaseVersion: '0.0.1',
packages: [
{
name: '@backstage/theme',
version: '5.0.0',
},
{
name: '@backstage/create-app',
version: '3.0.0',
},
],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: null, release: 'main' } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage/theme in b to ^5.0.0',
'Creating backstage.json',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage/theme : 1.0.0 ~> 5.0.0',
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(2);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/core',
);
expect(runObj.runPlain).not.toHaveBeenCalledWith(
'yarn',
'info',
'--json',
'@backstage/theme',
);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
expect(lockfileContents).toBe(lockfileMockResult);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5', // not bumped since new version is within range
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3', // not bumped
'@backstage/theme': '^5.0.0', // bumped since newer
},
});
});
it('should only bump packages in the manifest when a specific release is specified', async () => {
mockFs({
'/yarn.lock': lockfileMock,
'/package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'/packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '^1.0.5',
},
}),
'/packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '^1.0.3',
'@backstage/theme': '^1.0.0',
},
}),
});
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name: name,
'dist-tags': {
latest: REGISTRY_VERSIONS[name],
},
},
}),
);
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/releases/1.0.0/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
releaseVersion: '2.0.0',
packages: [
{ name: '@backstage/core', version: '5.0.0' },
{ name: '@backstage/core-api', version: '5.0.0' },
],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await expect(
bump({ pattern: null, release: '1.0.0' } as unknown as Command),
).rejects.toThrow('Duplicate versions present after package bump');
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Package info not found, ignoring package @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Package info not found, ignoring package @backstage/theme',
'Some packages are outdated, updating',
'bumping @backstage/core in a to ^5.0.0',
'bumping @backstage/core in b to ^5.0.0',
'Creating backstage.json',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage/core : 1.0.3 ~> 5.0.0',
' https://github.com/backstage/backstage/blob/master/packages/core/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
const packageA = await fs.readJson('/packages/a/package.json');
expect(packageA).toEqual({
name: 'a',
dependencies: {
'@backstage/core': '^5.0.0',
},
});
const packageB = await fs.readJson('/packages/b/package.json');
expect(packageB).toEqual({
name: 'b',
dependencies: {
'@backstage/core': '^5.0.0',
'@backstage/theme': '^1.0.0',
},
});
expect(await fs.readJson('/backstage.json')).toEqual({ version: '2.0.0' });
});
it('should bump backstage dependencies and dependencies matching pattern glob', async () => {
const customLockfileMock = `${lockfileMock}
"@backstage-extra/custom@^1.1.0":
@@ -246,25 +485,40 @@ describe('bump', () => {
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: '@{backstage,backstage-extra}/*' } as any);
await bump({
pattern: '@{backstage,backstage-extra}/*',
release: 'main',
} as any);
});
expect(logs.filter(Boolean)).toEqual([
'Using custom pattern glob @{backstage,backstage-extra}/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage-extra/custom-two in a to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'bumping @backstage-extra/custom-two in b to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'Skipping backstage.json update as custom pattern is used',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
@@ -273,7 +527,7 @@ describe('bump', () => {
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(6);
expect(runObj.runPlain).toHaveBeenCalledTimes(5);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
@@ -342,20 +596,31 @@ describe('bump', () => {
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async () => '');
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [],
}),
),
),
);
const { log: logs } = await withLogCollector(['log'], async () => {
await bump({ pattern: null } as unknown as Command);
await bump({ pattern: null, release: 'main' } as unknown as Command);
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'All Backstage packages are up to date!',
]);
@@ -393,27 +658,12 @@ describe('bumpBackstageJsonVersion', () => {
'/backstage.json': JSON.stringify({ version: '0.0.1' }),
});
paths.targetDir = '/';
const latest = '1.4.1';
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name,
'dist-tags': {
latest,
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
await bumpBackstageJsonVersion();
const json = await fs.readJson('/backstage.json');
expect(json).toEqual({ version: '1.4.1' });
await bumpBackstageJsonVersion('1.4.1');
expect(await fs.readJson('/backstage.json')).toEqual({ version: '1.4.1' });
});
it("should create backstage.json if doesn't exist", async () => {
@@ -423,22 +673,100 @@ describe('bumpBackstageJsonVersion', () => {
jest
.spyOn(paths, 'resolveTargetRoot')
.mockImplementation((...path) => resolvePath('/', ...path));
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
JSON.stringify({
type: 'inspect',
data: {
name,
'dist-tags': {
latest,
},
},
}),
);
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
await bumpBackstageJsonVersion();
const json = await fs.readJson('/backstage.json');
expect(json).toEqual({ version: '1.4.1' });
await bumpBackstageJsonVersion(latest);
expect(await fs.readJson('/backstage.json')).toEqual({ version: latest });
});
});
describe('createVersionFinder', () => {
async function findVersion(tag: string, data: Partial<YarnInfoInspectData>) {
const fetcher = () =>
Promise.resolve({
name: '@backstage/core',
'dist-tags': {},
versions: [],
time: {},
...data,
});
const versionFinder = createVersionFinder({
releaseLine: tag,
packageInfoFetcher: fetcher,
});
let result;
await withLogCollector(async () => {
result = await versionFinder('@backstage/core');
});
return result;
}
it('should create version finder', async () => {
await expect(
findVersion('latest', {
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
'dist-tags': { latest: '1.0.0' },
}),
).resolves.toBe('1.0.0');
await expect(
findVersion('main', {
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
'dist-tags': { latest: '1.0.0' },
}),
).resolves.toBe('1.0.0');
await expect(
findVersion('next', {
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
'dist-tags': { latest: '1.0.0' },
}),
).resolves.toBe('1.0.0');
await expect(
findVersion('next', {
time: {
'1.0.0': '2020-01-01T00:00:00.000Z',
'0.9.0': '2010-01-01T00:00:00.000Z',
},
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
}),
).resolves.toBe('1.0.0');
await expect(
findVersion('next', {
time: {
'1.0.0': '2020-01-01T00:00:00.000Z',
'0.9.0': '2020-02-01T00:00:00.000Z',
},
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
}),
).resolves.toBe('0.9.0');
await expect(findVersion('next', {})).rejects.toThrow(
"No target 'latest' version found for @backstage/core",
);
await expect(
findVersion('next', {
time: {
'0.9.0': '2020-02-01T00:00:00.000Z',
},
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
}),
).rejects.toThrow(
"No time available for version '1.0.0' of @backstage/core",
);
await expect(
findVersion('next', {
time: {
'1.0.0': '2020-01-01T00:00:00.000Z',
},
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
}),
).rejects.toThrow(
"No time available for version '0.9.0' of @backstage/core",
);
});
});
+193 -115
View File
@@ -19,7 +19,7 @@ import chalk from 'chalk';
import semver from 'semver';
import minimatch from 'minimatch';
import { Command } from 'commander';
import { isError } from '@backstage/errors';
import { isError, NotFoundError } from '@backstage/errors';
import { resolve as resolvePath } from 'path';
import { run } from '../../lib/run';
import { paths } from '../../lib/paths';
@@ -27,9 +27,16 @@ import {
mapDependencies,
fetchPackageInfo,
Lockfile,
YarnInfoInspectData,
} from '../../lib/versioning';
import { forbiddenDuplicatesFilter } from './lint';
import { BACKSTAGE_JSON } from '@backstage/cli-common';
import { runParallelWorkers } from '../../lib/parallel';
import {
getManifestByReleaseLine,
getManifestByVersion,
ReleaseManifest,
} from '@backstage/release-manifests';
const DEP_TYPES = [
'dependencies',
@@ -59,7 +66,22 @@ export default async (cmd: Command) => {
console.log(`Using custom pattern glob ${pattern}`);
}
const findTargetVersion = createVersionFinder();
let findTargetVersion: (name: string) => Promise<string>;
let releaseManifest: ReleaseManifest;
if (semver.valid(cmd.release)) {
releaseManifest = await getManifestByVersion({ version: cmd.release });
findTargetVersion = createStrictVersionFinder({
releaseManifest,
});
} else {
releaseManifest = await getManifestByReleaseLine({
releaseLine: cmd.release,
});
findTargetVersion = createVersionFinder({
releaseLine: cmd.releaseLine,
releaseManifest,
});
}
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(paths.targetDir, pattern);
@@ -68,67 +90,76 @@ export default async (cmd: Command) => {
const versionBumps = new Map<string, PkgVersionInfo[]>();
// Track package versions that we want to remove from yarn.lock in order to trigger a bump
const unlocked = Array<{ name: string; range: string; target: string }>();
await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => {
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
for (const pkg of pkgs) {
if (semver.satisfies(target, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== target) {
unlocked.push({ name, range: pkg.range, target });
await runParallelWorkers({
parallelismFactor: 4,
items: dependencyMap.entries(),
async worker([name, pkgs]) {
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
continue;
throw error;
}
versionBumps.set(
pkg.name,
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
target,
}),
);
}
for (const pkg of pkgs) {
if (semver.satisfies(target, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== target) {
unlocked.push({ name, range: pkg.range, target });
}
continue;
}
versionBumps.set(
pkg.name,
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
target,
}),
);
}
},
});
const filter = (name: string) => minimatch(name, pattern);
// Check for updates of transitive backstage dependencies
await workerThreads(16, lockfile.keys(), async name => {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!filter(name)) {
return;
}
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
await runParallelWorkers({
parallelismFactor: 4,
items: lockfile.keys(),
async worker(name) {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!filter(name)) {
return;
}
throw error;
}
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
// these can't cause the package to be locked to an older version
if (!semver.satisfies(target, entry.range)) {
continue;
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
// Unlock all entries that are within range but on the old version
unlocked.push({ name, range: entry.range, target });
}
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
// these can't cause the package to be locked to an older version
if (!semver.satisfies(target, entry.range)) {
continue;
}
// Unlock all entries that are within range but on the old version
unlocked.push({ name, range: entry.range, target });
}
},
});
console.log();
@@ -163,44 +194,56 @@ export default async (cmd: Command) => {
}
const breakingUpdates = new Map<string, { from: string; to: string }>();
await workerThreads(16, versionBumps.entries(), async ([name, deps]) => {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
await runParallelWorkers({
parallelismFactor: 4,
items: versionBumps.entries(),
async worker([name, deps]) {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
for (const dep of deps) {
console.log(
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
name,
)} to ${chalk.yellow(dep.range)}`,
);
for (const dep of deps) {
console.log(
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
name,
)} to ${chalk.yellow(dep.range)}`,
);
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
if (!semver.satisfies(to, `^${from}`)) {
breakingUpdates.set(dep.name, { from, to });
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
if (!semver.satisfies(to, `^${from}`)) {
breakingUpdates.set(dep.name, { from, to });
}
}
}
}
}
}
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
},
});
console.log();
await bumpBackstageJsonVersion();
// Do not update backstage.json when upgrade patterns are used.
if (pattern === DEFAULT_PATTERN_GLOB) {
await bumpBackstageJsonVersion(releaseManifest.releaseVersion);
} else {
console.log(
chalk.yellow(
`Skipping backstage.json update as custom pattern is used`,
),
);
}
console.log();
console.log(
`Running ${chalk.blue('yarn install')} to install new versions`,
@@ -270,9 +313,38 @@ export default async (cmd: Command) => {
}
};
function createVersionFinder() {
const found = new Map<string, string>();
export function createStrictVersionFinder(options: {
releaseManifest: ReleaseManifest;
}) {
const releasePackages = new Map(
options.releaseManifest.packages.map(p => [p.name, p.version]),
);
return async function findTargetVersion(name: string) {
console.log(`Checking for updates of ${name}`);
const manifestVersion = releasePackages.get(name);
if (manifestVersion) {
return manifestVersion;
}
throw new NotFoundError(`Package ${name} not found in release manifest`);
};
}
export function createVersionFinder(options: {
releaseLine?: string;
packageInfoFetcher?: () => Promise<YarnInfoInspectData>;
releaseManifest?: ReleaseManifest;
}) {
const {
releaseLine = 'latest',
packageInfoFetcher = fetchPackageInfo,
releaseManifest,
} = options;
// The main release line is just an alias for latest
const distTag = releaseLine === 'main' ? 'latest' : releaseLine;
const found = new Map<string, string>();
const releasePackages = new Map(
releaseManifest?.packages.map(p => [p.name, p.version]),
);
return async function findTargetVersion(name: string) {
const existing = found.get(name);
if (existing) {
@@ -280,17 +352,50 @@ function createVersionFinder() {
}
console.log(`Checking for updates of ${name}`);
const info = await fetchPackageInfo(name);
const latest = info['dist-tags'].latest;
if (!latest) {
throw new Error(`No latest version found for ${name}`);
const manifestVersion = releasePackages.get(name);
if (manifestVersion) {
return manifestVersion;
}
found.set(name, latest);
return latest;
const info = await packageInfoFetcher(name);
const latestVersion = info['dist-tags'].latest;
if (!latestVersion) {
throw new Error(`No target 'latest' version found for ${name}`);
}
const taggedVersion = info['dist-tags'][distTag];
if (distTag === 'latest' || !taggedVersion) {
found.set(name, latestVersion);
return latestVersion;
}
const latestVersionDateStr = info.time[latestVersion];
const taggedVersionDateStr = info.time[taggedVersion];
if (!latestVersionDateStr) {
throw new Error(
`No time available for version '${latestVersion}' of ${name}`,
);
}
if (!taggedVersionDateStr) {
throw new Error(
`No time available for version '${taggedVersion}' of ${name}`,
);
}
const latestVersionRelease = new Date(latestVersionDateStr).getTime();
const taggedVersionRelease = new Date(taggedVersionDateStr).getTime();
if (latestVersionRelease > taggedVersionRelease) {
// Prefer latest version if it's newer.
found.set(name, latestVersion);
return latestVersion;
}
found.set(name, taggedVersion);
return taggedVersion;
};
}
export async function bumpBackstageJsonVersion() {
export async function bumpBackstageJsonVersion(version: string) {
const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON);
const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => {
if (e.code === 'ENOENT') {
@@ -300,10 +405,7 @@ export async function bumpBackstageJsonVersion() {
throw e;
});
const info = await fetchPackageInfo('@backstage/create-app');
const { latest } = info['dist-tags'];
if (backstageJson?.version === latest) {
if (backstageJson?.version === version) {
return;
}
@@ -317,34 +419,10 @@ export async function bumpBackstageJsonVersion() {
await fs.writeJson(
backstageJsonPath,
{ ...backstageJson, version: latest },
{ ...backstageJson, version },
{
spaces: 2,
encoding: 'utf8',
},
);
}
async function workerThreads<T>(
count: number,
items: IterableIterator<T>,
fn: (item: T) => Promise<void>,
) {
const queue = Array.from(items);
async function pop() {
const item = queue.pop();
if (!item) {
return;
}
await fn(item);
await pop();
}
return Promise.all(
Array(count)
.fill(0)
.map(() => pop()),
);
}
@@ -16,146 +16,127 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import {
relative as relativePath,
resolve as resolvePath,
dirname,
} from 'path';
import { Worker } from 'worker_threads';
import { relative as relativePath, resolve as resolvePath } from 'path';
import { paths } from '../paths';
import { buildTypeDefinitionsWorker } from './buildTypeDefinitionsWorker';
// These message types are ignored since we want to avoid duplicating the logic of
// handling them correctly, and we already have the API Reports warning about them.
const ignoredMessages = new Set(['tsdoc-undefined-tag', 'ae-forgotten-export']);
let apiExtractor: undefined | typeof import('@microsoft/api-extractor');
function prepareApiExtractor() {
if (apiExtractor) {
return apiExtractor;
}
try {
apiExtractor = require('@microsoft/api-extractor');
} catch (error) {
throw new Error(
'Failed to resolve @microsoft/api-extractor, it must best installed ' +
'as a dependency of your project in order to use experimental type builds',
);
}
/**
* All of this monkey patching below is because MUI has these bare package.json file as a method
* for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking
* by declaring them side effect free.
*
* The package.json lookup logic in api-extractor really doesn't like that though, as it enforces
* that the 'name' field exists in all package.json files that it discovers. This below is just
* making sure that we ignore those file package.json files instead of crashing.
*/
const {
PackageJsonLookup,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('@rushstack/node-core-library/lib/PackageJsonLookup');
const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor;
PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor =
function tryGetPackageJsonFilePathForPatch(path: string) {
if (
path.includes('@material-ui') &&
!dirname(path).endsWith('@material-ui')
) {
return undefined;
}
return old.call(this, path);
};
return apiExtractor!;
}
export async function buildTypeDefinitions() {
const { Extractor, ExtractorConfig } = prepareApiExtractor();
const distTypesPackageDir = paths.resolveTargetRoot(
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
export async function buildTypeDefinitions(
targetDirs: string[] = [paths.targetDir],
) {
const packageDirs = targetDirs.map(dir =>
relativePath(paths.targetRoot, dir),
);
const entryPoint = resolvePath(distTypesPackageDir, 'src/index.d.ts');
const entryPoints = await Promise.all(
packageDirs.map(async dir => {
const entryPoint = paths.resolveTargetRoot(
'dist-types',
dir,
'src/index.d.ts',
);
const declarationsExist = await fs.pathExists(entryPoint);
if (!declarationsExist) {
const path = relativePath(paths.targetDir, entryPoint);
throw new Error(
`No declaration files found at ${path}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
)} to generate .d.ts files before packaging`,
);
}
const declarationsExist = await fs.pathExists(entryPoint);
if (!declarationsExist) {
throw new Error(
`No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
)} to generate .d.ts files before packaging`,
);
}
return entryPoint;
}),
);
const extractorConfig = ExtractorConfig.prepare({
configObject: {
mainEntryPointFilePath: entryPoint,
bundledPackages: [],
const workerConfigs = packageDirs.map(packageDir => {
const targetDir = paths.resolveTargetRoot(packageDir);
const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir);
const extractorOptions = {
configObject: {
mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'),
bundledPackages: [],
compiler: {
skipLibCheck: true,
tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
compiler: {
skipLibCheck: true,
tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
},
dtsRollup: {
enabled: true,
untrimmedFilePath: resolvePath(targetDir, 'dist/index.alpha.d.ts'),
betaTrimmedFilePath: resolvePath(targetDir, 'dist/index.beta.d.ts'),
publicTrimmedFilePath: resolvePath(targetDir, 'dist/index.d.ts'),
},
newlineKind: 'lf',
projectFolder: targetDir,
},
dtsRollup: {
enabled: true,
untrimmedFilePath: paths.resolveTarget('dist/index.alpha.d.ts'),
betaTrimmedFilePath: paths.resolveTarget('dist/index.beta.d.ts'),
publicTrimmedFilePath: paths.resolveTarget('dist/index.d.ts'),
},
newlineKind: 'lf',
projectFolder: paths.targetDir,
},
configObjectFullPath: paths.targetDir,
packageJsonFullPath: paths.resolveTarget('package.json'),
configObjectFullPath: targetDir,
packageJsonFullPath: resolvePath(targetDir, 'package.json'),
};
return { extractorOptions, targetTypesDir };
});
const typescriptDir = paths.resolveTargetRoot('node_modules/typescript');
const hasTypescript = await fs.pathExists(typescriptDir);
const extractorResult = Extractor.invoke(extractorConfig, {
typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined,
localBuild: false,
showVerboseMessages: false,
showDiagnostics: false,
messageCallback(message) {
message.handled = true;
if (ignoredMessages.has(message.messageId)) {
return;
}
const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined;
let text = `${message.text} (${message.messageId})`;
if (message.sourceFilePath) {
text += ' at ';
text += relativePath(distTypesPackageDir, message.sourceFilePath);
if (message.sourceFileLine) {
text += `:${message.sourceFileLine}`;
if (message.sourceFileColumn) {
text += `:${message.sourceFileColumn}`;
}
}
}
if (message.logLevel === 'error') {
console.error(chalk.red(`Error: ${text}`));
} else if (
message.logLevel === 'warning' ||
message.category === 'Extractor'
) {
console.warn(`Warning: ${text}`);
} else {
console.log(text);
}
const worker = new Worker(`(${buildTypeDefinitionsWorker})()`, {
eval: true,
workerData: {
entryPoints,
workerConfigs,
typescriptCompilerFolder,
},
});
if (!extractorResult.succeeded) {
throw new Error(
`Type definition build completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
}
await new Promise<void>((resolve, reject) => {
worker.once('error', reject);
worker.once('exit', code => {
if (code) {
reject(new Error(`Worker exited with code ${code}`));
}
});
worker.on('message', data => {
if (data.type === 'done') {
if (data.error) {
reject(data.error);
} else {
resolve();
}
} else if (data.type === 'message') {
const { message, targetTypesDir } = data;
if (ignoredMessages.has(message.messageId)) {
return;
}
let text = `${message.text} (${message.messageId})`;
if (message.sourceFilePath) {
text += ' at ';
text += relativePath(targetTypesDir, message.sourceFilePath);
if (message.sourceFileLine) {
text += `:${message.sourceFileLine}`;
if (message.sourceFileColumn) {
text += `:${message.sourceFileColumn}`;
}
}
}
if (message.logLevel === 'error') {
console.error(chalk.red(`Error: ${text}`));
} else if (
message.logLevel === 'warning' ||
message.category === 'Extractor'
) {
console.warn(`Warning: ${text}`);
} else {
console.log(text);
}
}
});
});
}
@@ -0,0 +1,105 @@
/*
* 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.
*/
/**
* NOTE: This is a worker thread function that is stringified and executed
* withing a `worker_threads.Worker`. Everything in this function must
* be self-contained.
* Using TypeScript is fine as it is transpiled before being stringified.
*/
export function buildTypeDefinitionsWorker() {
try {
require('@microsoft/api-extractor');
} catch (error) {
throw new Error(
'Failed to resolve @microsoft/api-extractor, it must best installed ' +
'as a dependency of your project in order to use experimental type builds',
);
}
const { dirname } = require('path');
const { workerData, parentPort } = require('worker_threads');
const { entryPoints, workerConfigs, typescriptCompilerFolder } = workerData;
const apiExtractor = require('@microsoft/api-extractor');
const { Extractor, ExtractorConfig, CompilerState } = apiExtractor;
/**
* All of this monkey patching below is because MUI has these bare package.json file as a method
* for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking
* by declaring them side effect free.
*
* The package.json lookup logic in api-extractor really doesn't like that though, as it enforces
* that the 'name' field exists in all package.json files that it discovers. This below is just
* making sure that we ignore those file package.json files instead of crashing.
*/
const {
PackageJsonLookup,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('@rushstack/node-core-library/lib/PackageJsonLookup');
const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor;
PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor =
function tryGetPackageJsonFilePathForPatch(path: string) {
if (
path.includes('@material-ui') &&
!dirname(path).endsWith('@material-ui')
) {
return undefined;
}
return old.call(this, path);
};
let success = true;
let compilerState;
for (const { extractorOptions, targetTypesDir } of workerConfigs) {
const extractorConfig = ExtractorConfig.prepare(extractorOptions);
if (!compilerState) {
compilerState = CompilerState.create(extractorConfig, {
additionalEntryPoints: entryPoints,
});
}
const extractorResult = Extractor.invoke(extractorConfig, {
compilerState,
localBuild: false,
typescriptCompilerFolder,
showVerboseMessages: false,
showDiagnostics: false,
messageCallback: (message: any) => {
message.handled = true;
parentPort.postMessage({ type: 'message', message, targetTypesDir });
},
});
if (!extractorResult.succeeded) {
parentPort.postMessage({
type: 'done',
error: new Error(
`Type definition build completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
),
});
success = false;
break;
}
}
if (success) {
parentPort.postMessage({ type: 'done' });
}
}
+24 -8
View File
@@ -16,7 +16,7 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { relative as relativePath } from 'path';
import { relative as relativePath, resolve as resolvePath } from 'path';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
@@ -26,7 +26,7 @@ import svgr from '@svgr/rollup';
import dts from 'rollup-plugin-dts';
import json from '@rollup/plugin-json';
import yaml from '@rollup/plugin-yaml';
import { RollupOptions, OutputOptions } from 'rollup';
import { RollupOptions, OutputOptions, RollupWarning } from 'rollup';
import { forwardFileImports } from './plugins';
import { BuildOptions, Output } from './types';
@@ -37,6 +37,19 @@ export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
const targetDir = options.targetDir ?? paths.targetDir;
const onwarn = ({ code, message }: RollupWarning) => {
if (code === 'EMPTY_BUNDLE') {
return; // We don't care about this one
}
if (options.logPrefix) {
console.log(options.logPrefix + message);
} else {
console.log(message);
}
};
const distDir = resolvePath(targetDir, 'dist');
if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) {
const output = new Array<OutputOptions>();
@@ -44,7 +57,7 @@ export async function makeRollupConfigs(
if (options.outputs.has(Output.cjs)) {
output.push({
dir: 'dist',
dir: distDir,
entryFileNames: 'index.cjs.js',
chunkFileNames: 'cjs/[name]-[hash].cjs.js',
format: 'commonjs',
@@ -53,7 +66,7 @@ export async function makeRollupConfigs(
}
if (options.outputs.has(Output.esm)) {
output.push({
dir: 'dist',
dir: distDir,
entryFileNames: 'index.esm.js',
chunkFileNames: 'esm/[name]-[hash].esm.js',
format: 'module',
@@ -64,12 +77,14 @@ export async function makeRollupConfigs(
}
configs.push({
input: 'src/index.ts',
input: resolvePath(targetDir, 'src/index.ts'),
output,
onwarn,
preserveEntrySignatures: 'strict',
external: require('module').builtinModules,
plugins: [
peerDepsExternal({
packageJsonPath: resolvePath(targetDir, 'package.json'),
includeDependencies: true,
}),
resolve({ mainFields }),
@@ -109,13 +124,13 @@ export async function makeRollupConfigs(
if (options.outputs.has(Output.types) && !options.useApiExtractor) {
const typesInput = paths.resolveTargetRoot(
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
relativePath(paths.targetRoot, targetDir),
'src/index.d.ts',
);
const declarationsExist = await fs.pathExists(typesInput);
if (!declarationsExist) {
const path = relativePath(paths.targetDir, typesInput);
const path = relativePath(targetDir, typesInput);
throw new Error(
`No declaration files found at ${path}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
@@ -126,9 +141,10 @@ export async function makeRollupConfigs(
configs.push({
input: typesInput,
output: {
file: 'dist/index.d.ts',
file: resolvePath(distDir, 'index.d.ts'),
format: 'es',
},
onwarn,
plugins: [dts()],
});
}
+1 -1
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { buildPackage } from './packager';
export { buildPackage, buildPackages, getOutputsForRole } from './packager';
export { Output } from './types';
export type { BuildOptions } from './types';
+51 -1
View File
@@ -17,11 +17,13 @@
import fs from 'fs-extra';
import { rollup, RollupOptions } from 'rollup';
import chalk from 'chalk';
import { relative as relativePath } from 'path';
import { relative as relativePath, resolve as resolvePath } from 'path';
import { paths } from '../paths';
import { makeRollupConfigs } from './config';
import { BuildOptions, Output } from './types';
import { buildTypeDefinitions } from './buildTypeDefinitions';
import { getRoleInfo } from '../role';
import { runParallelWorkers } from '../parallel';
export function formatErrorMessage(error: any) {
let msg = '';
@@ -116,3 +118,51 @@ export const buildPackage = async (options: BuildOptions) => {
await Promise.all(buildTasks);
};
export const buildPackages = async (options: BuildOptions[]) => {
if (options.some(opt => !opt.targetDir)) {
throw new Error('targetDir must be set for all build options');
}
const rollupConfigs = await Promise.all(options.map(makeRollupConfigs));
await Promise.all(
options.map(({ targetDir }) => fs.remove(resolvePath(targetDir!, 'dist'))),
);
const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts));
const typeDefinitionTargetDirs = options
.filter(
({ outputs, useApiExtractor }) =>
outputs.has(Output.types) && useApiExtractor,
)
.map(_ => _.targetDir!);
if (typeDefinitionTargetDirs.length > 0) {
// Make sure this one is started first
buildTasks.unshift(() => buildTypeDefinitions(typeDefinitionTargetDirs));
}
await runParallelWorkers({
items: buildTasks,
worker: async task => task(),
});
};
export function getOutputsForRole(role: string): Set<Output> {
const outputs = new Set<Output>();
for (const output of getRoleInfo(role).output) {
if (output === 'cjs') {
outputs.add(Output.cjs);
}
if (output === 'esm') {
outputs.add(Output.esm);
}
if (output === 'types') {
outputs.add(Output.types);
}
}
return outputs;
}
+2
View File
@@ -21,6 +21,8 @@ export enum Output {
}
export type BuildOptions = {
logPrefix?: string;
targetDir?: string;
outputs: Set<Output>;
minify?: boolean;
useApiExtractor?: boolean;
+1 -1
View File
@@ -93,7 +93,7 @@ export async function createConfig(
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: { configFile: paths.targetTsConfig },
typescript: { configFile: paths.targetTsConfig, memoryLimit: 4096 },
}),
new ESLintPlugin({
context: paths.targetPath,
+16 -13
View File
@@ -15,55 +15,58 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { paths } from '../paths';
export type BundlingPathsOptions = {
// bundle entrypoint, e.g. 'src/index'
entry: string;
// Target directory, defaulting to paths.targetDir
targetDir?: string;
};
export function resolveBundlingPaths(options: BundlingPathsOptions) {
const { entry } = options;
const { entry, targetDir = paths.targetDir } = options;
const resolveTargetModule = (pathString: string) => {
for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) {
const filePath = paths.resolveTarget(`${pathString}.${ext}`);
const filePath = resolvePath(targetDir, `${pathString}.${ext}`);
if (fs.pathExistsSync(filePath)) {
return filePath;
}
}
return paths.resolveTarget(`${pathString}.js`);
return resolvePath(targetDir, `${pathString}.js`);
};
let targetPublic = undefined;
let targetHtml = paths.resolveTarget('public/index.html');
let targetHtml = resolvePath(targetDir, 'public/index.html');
// Prefer public folder
if (fs.pathExistsSync(targetHtml)) {
targetPublic = paths.resolveTarget('public');
targetPublic = resolvePath(targetDir, 'public');
} else {
targetHtml = paths.resolveTarget(`${entry}.html`);
targetHtml = resolvePath(targetDir, `${entry}.html`);
if (!fs.pathExistsSync(targetHtml)) {
targetHtml = paths.resolveOwn('templates/serve_index.html');
}
}
// Backend plugin dev run file
const targetRunFile = paths.resolveTarget('src/run.ts');
const targetRunFile = resolvePath(targetDir, 'src/run.ts');
const runFileExists = fs.pathExistsSync(targetRunFile);
return {
targetHtml,
targetPublic,
targetPath: paths.resolveTarget('.'),
targetPath: resolvePath(targetDir, '.'),
targetRunFile: runFileExists ? targetRunFile : undefined,
targetDist: paths.resolveTarget('dist'),
targetAssets: paths.resolveTarget('assets'),
targetSrc: paths.resolveTarget('src'),
targetDev: paths.resolveTarget('dev'),
targetDist: resolvePath(targetDir, 'dist'),
targetAssets: resolvePath(targetDir, 'assets'),
targetSrc: resolvePath(targetDir, 'src'),
targetDev: resolvePath(targetDir, 'dev'),
targetEntry: resolveTargetModule(entry),
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
targetPackageJson: paths.resolveTarget('package.json'),
targetPackageJson: resolvePath(targetDir, 'package.json'),
rootNodeModules: paths.resolveTargetRoot('node_modules'),
root: paths.targetRoot,
};
+5 -4
View File
@@ -16,7 +16,6 @@
import { AppConfig, Config } from '@backstage/config';
import { BundlingPathsOptions } from './paths';
import { ParallelOption } from '../parallel';
import { ConfigSchema } from '@backstage/config-loader';
export type BundlingOptions = {
@@ -25,7 +24,7 @@ export type BundlingOptions = {
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
baseUrl: URL;
parallel?: ParallelOption;
parallelism?: number;
};
export type ServeOptions = BundlingPathsOptions & {
@@ -35,8 +34,10 @@ export type ServeOptions = BundlingPathsOptions & {
};
export type BuildOptions = BundlingPathsOptions & {
// Target directory, defaulting to paths.targetDir
targetDir?: string;
statsJsonEnabled: boolean;
parallel?: ParallelOption;
parallelism?: number;
schema?: ConfigSchema;
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
@@ -45,7 +46,7 @@ export type BuildOptions = BundlingPathsOptions & {
export type BackendBundlingOptions = {
checksEnabled: boolean;
isDev: boolean;
parallel?: ParallelOption;
parallelism?: number;
inspectEnabled: boolean;
inspectBrkEnabled: boolean;
};
@@ -0,0 +1,88 @@
/*
* 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 fs from 'fs-extra';
import npmPackList from 'npm-packlist';
import { join as joinPath, resolve as resolvePath } from 'path';
const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes'];
// Writes e.g. alpha/package.json
async function writeReleaseStageEntrypoint(
pkg: any,
stage: 'alpha' | 'beta',
targetDir: string,
) {
await fs.ensureDir(resolvePath(targetDir, stage));
await fs.writeJson(
resolvePath(targetDir, stage, 'package.json'),
{
name: pkg.name,
version: pkg.version,
main: (pkg.publishConfig.main || pkg.main) && '..',
module: (pkg.publishConfig.module || pkg.module) && '..',
browser: (pkg.publishConfig.browser || pkg.browser) && '..',
types: joinPath('..', pkg.publishConfig[`${stage}Types`]),
},
{ encoding: 'utf8', spaces: 2 },
);
}
export async function copyPackageDist(packageDir: string, targetDir: string) {
const pkgPath = resolvePath(packageDir, 'package.json');
const pkgContent = await fs.readFile(pkgPath, 'utf8');
const pkg = JSON.parse(pkgContent);
const publishConfig = pkg.publishConfig ?? {};
for (const key of Object.keys(publishConfig)) {
if (!SKIPPED_KEYS.includes(key)) {
pkg[key] = publishConfig[key];
}
}
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (pkg.bundled) {
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.peerDependencies;
delete pkg.optionalDependencies;
}
// Write the modified package.json so that the file listing is correct
await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 });
// Lists all dist files, respecting .npmignore, files field in package.json, etc.
const filePaths = await npmPackList({ path: packageDir });
await fs.ensureDir(targetDir);
for (const filePath of filePaths.sort()) {
await fs.copy(
resolvePath(packageDir, filePath),
resolvePath(targetDir, filePath),
);
}
if (publishConfig.alphaTypes) {
await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir);
}
if (publishConfig.betaTypes) {
await writeReleaseStageEntrypoint(pkg, 'beta', targetDir);
}
// Restore package.json
await fs.writeFile(pkgPath, pkgContent, 'utf8');
}
@@ -0,0 +1,303 @@
/*
* 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 chalk from 'chalk';
import fs from 'fs-extra';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { tmpdir } from 'os';
import tar, { CreateOptions } from 'tar';
import partition from 'lodash/partition';
import { paths } from '../paths';
import { run } from '../run';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
} from '../../../package.json';
import { PackageGraph, PackageGraphNode } from '../monorepo';
import {
BuildOptions,
buildPackages,
getOutputsForRole,
Output,
} from '../builder';
import { copyPackageDist } from './copyPackageDist';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
...Object.keys(cliDependencies),
...Object.keys(cliDevDependencies),
];
type FileEntry =
| string
| {
src: string;
dest: string;
};
type Options = {
/**
* Target directory for the dist workspace, defaults to a temporary directory
*/
targetDir?: string;
/**
* Files to copy into the target workspace.
*
* Defaults to ['yarn.lock', 'package.json'].
*/
files?: FileEntry[];
/**
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* When `buildDependencies` is set, this list of packages will not be built even if they are dependencies.
*/
buildExcludes?: string[];
/**
* Controls amount of parallelism in some build steps.
*/
parallelism?: number;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar' | 'skeleton.tar.gz';
};
/**
* Uses `yarn pack` to package local packages and unpacks them into a dist workspace.
* The target workspace will end up containing dist version of each package and
* will be suitable for packaging e.g. into a docker image.
*
* This creates a structure that is functionally similar to if the packages were
* installed from npm, but uses Yarn workspaces to link to them at runtime.
*/
export async function createDistWorkspace(
packageNames: string[],
options: Options = {},
) {
const targetDir =
options.targetDir ??
(await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace')));
const packages = await PackageGraph.listTargetPackages();
const packageGraph = PackageGraph.fromPackages(packages);
const targetNames = packageGraph.collectPackageNames(packageNames, node => {
// Don't include dependencies of packages that are marked as bundled
if (node.packageJson.bundled) {
return undefined;
}
return node.publishedLocalDependencies.keys();
});
const targets = Array.from(targetNames).map(name => packageGraph.get(name)!);
if (options.buildDependencies) {
const exclude = options.buildExcludes ?? [];
const toBuild = new Set(
targets.map(_ => _.name).filter(name => !exclude.includes(name)),
);
const standardBuilds = new Array<BuildOptions>();
const customBuild = new Array<string>();
for (const pkg of packages) {
if (!toBuild.has(pkg.packageJson.name)) {
continue;
}
const role = pkg.packageJson.backstage?.role;
if (!role) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no role`);
customBuild.push(pkg.packageJson.name);
continue;
}
const buildScript = pkg.packageJson.scripts?.build;
if (!buildScript) {
customBuild.push(pkg.packageJson.name);
continue;
}
if (!buildScript.startsWith('backstage-cli script build')) {
console.warn(
`Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`,
);
customBuild.push(pkg.packageJson.name);
continue;
}
const outputs = getOutputsForRole(role);
// No need to build and include types in the production runtime
outputs.delete(Output.types);
if (outputs.size > 0) {
standardBuilds.push({
targetDir: pkg.dir,
outputs: outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
// No need to detect these for the backend builds, we assume no minification or types
minify: false,
useApiExtractor: false,
});
}
}
await buildPackages(standardBuilds);
if (customBuild.length > 0) {
const scopeArgs = customBuild.flatMap(name => ['--scope', name]);
const lernaArgs =
options.parallelism && Number.isInteger(options.parallelism)
? ['--concurrency', options.parallelism.toString()]
: [];
await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], {
cwd: paths.targetRoot,
});
}
}
await moveToDistWorkspace(targetDir, targets);
const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json'];
for (const file of files) {
const src = typeof file === 'string' ? file : file.src;
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.dir);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
gzip: options.skeleton.endsWith('.gz'),
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
const FAST_PACK_SCRIPTS = [
undefined,
'backstage-cli prepack',
'backstage-cli script prepack',
];
async function moveToDistWorkspace(
workspaceDir: string,
localPackages: PackageGraphNode[],
): Promise<void> {
const [fastPackPackages, slowPackPackages] = partition(localPackages, pkg =>
FAST_PACK_SCRIPTS.includes(pkg.packageJson.scripts?.prepack),
);
// New an improved flow where we avoid calling `yarn pack`
await Promise.all(
fastPackPackages.map(async target => {
console.log(`Moving ${target.name} into dist workspace`);
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await copyPackageDist(target.dir, absoluteOutputPath);
}),
);
// Old flow is below, which calls `yarn pack` and extracts the tarball
async function pack(target: PackageGraphNode, archive: string) {
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
cwd: target.dir,
});
// TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed
if (target.packageJson?.scripts?.postpack) {
await run('yarn', ['postpack'], { cwd: target.dir });
}
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
await tar.extract({
file: archivePath,
cwd: absoluteOutputPath,
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.packageJson.bundled) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}
const [unsafePackages, safePackages] = partition(slowPackPackages, p =>
UNSAFE_PACKAGES.includes(p.name),
);
// The unsafe package are packed first one by one in order to avoid race conditions
// where the CLI is being executed with broken dependencies.
for (const target of unsafePackages) {
await pack(target, `temp-package.tgz`);
}
// Repacking in parallel is much faster and safe for all packages outside of the Backstage repo
await Promise.all(
safePackages.map(async (target, index) =>
pack(target, `temp-package-${index}.tgz`),
),
);
}
+2 -211
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* 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.
@@ -14,213 +14,4 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { tmpdir } from 'os';
import tar, { CreateOptions } from 'tar';
import { paths } from '../paths';
import { run } from '../run';
import { ParallelOption } from '../parallel';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
} from '../../../package.json';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph, PackageGraphNode } from '../monorepo';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
...Object.keys(cliDependencies),
...Object.keys(cliDevDependencies),
];
type FileEntry =
| string
| {
src: string;
dest: string;
};
type Options = {
/**
* Target directory for the dist workspace, defaults to a temporary directory
*/
targetDir?: string;
/**
* Files to copy into the target workspace.
*
* Defaults to ['yarn.lock', 'package.json'].
*/
files?: FileEntry[];
/**
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* When `buildDependencies` is set, this list of packages will not be built even if they are dependencies.
*/
buildExcludes?: string[];
/**
* Enable (true/false) or control amount of (number) parallelism in some build steps.
*/
parallel?: ParallelOption;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar' | 'skeleton.tar.gz';
};
/**
* Uses `yarn pack` to package local packages and unpacks them into a dist workspace.
* The target workspace will end up containing dist version of each package and
* will be suitable for packaging e.g. into a docker image.
*
* This creates a structure that is functionally similar to if the packages where
* installed from npm, but uses Yarn workspaces to link to them at runtime.
*/
export async function createDistWorkspace(
packageNames: string[],
options: Options = {},
) {
const targetDir =
options.targetDir ??
(await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace')));
const { packages } = await getPackages(paths.targetDir);
const packageGraph = PackageGraph.fromPackages(packages);
const targetNames = packageGraph.collectPackageNames(packageNames, node => {
// Don't include dependencies of packages that are marked as bundled
if (node.packageJson.bundled) {
return undefined;
}
return node.publishedLocalDependencies.keys();
});
const targets = Array.from(targetNames).map(name => packageGraph.get(name)!);
if (options.buildDependencies) {
const exclude = options.buildExcludes ?? [];
const toBuild = targets.filter(target => !exclude.includes(target.name));
if (toBuild.length > 0) {
const scopeArgs = toBuild.flatMap(target => ['--scope', target.name]);
const lernaArgs =
options.parallel && Number.isInteger(options.parallel)
? ['--concurrency', options.parallel.toString()]
: [];
await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], {
cwd: paths.targetRoot,
});
}
}
await moveToDistWorkspace(targetDir, targets);
const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json'];
for (const file of files) {
const src = typeof file === 'string' ? file : file.src;
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.dir);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
gzip: options.skeleton.endsWith('.gz'),
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
async function moveToDistWorkspace(
workspaceDir: string,
localPackages: PackageGraphNode[],
): Promise<void> {
async function pack(target: PackageGraphNode, archive: string) {
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
cwd: target.dir,
});
// TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed
if (target.packageJson?.scripts?.postpack) {
await run('yarn', ['postpack'], { cwd: target.dir });
}
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
await tar.extract({
file: archivePath,
cwd: absoluteOutputPath,
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.packageJson.bundled) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}
const unsafePackages = localPackages.filter(p =>
UNSAFE_PACKAGES.includes(p.name),
);
const safePackages = localPackages.filter(
p => !UNSAFE_PACKAGES.includes(p.name),
);
// The unsafe package are packed first one by one in order to avoid race conditions
// where the CLI is being executed with broken dependencies.
for (const target of unsafePackages) {
await pack(target, `temp-package.tgz`);
}
// Repacking in parallel is much faster and safe for all packages outside of the Backstage repo
await Promise.all(
safePackages.map(async (target, index) =>
pack(target, `temp-package-${index}.tgz`),
),
);
}
export { createDistWorkspace } from './createDistWorkspace';
+122 -37
View File
@@ -14,46 +14,131 @@
* limitations under the License.
*/
import { isParallelDefault, parseParallel } from './parallel';
import {
parseParallelismOption,
getEnvironmentParallelism,
runParallelWorkers,
} from './parallel';
describe('parallel', () => {
describe('parseParallel', () => {
it('coerces "false" string to boolean', () => {
expect(parseParallel('false')).toBeFalsy();
});
it('coerces "true" to boolean', () => {
expect(parseParallel('true')).toBeTruthy();
});
it('coerces number string to number', () => {
expect(parseParallel('2')).toBe(2);
});
it.each([[true], [false], [2]])('returns itself for %p', value => {
expect(parseParallel(value as any)).toEqual(value);
});
it.each([[undefined], [null]])('returns true for %p', value => {
expect(parseParallel(value as any)).toBe(true);
});
it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => {
expect(() => parseParallel(value as any)).toThrowError(
`Parallel option value '${value}' is not a boolean or integer`,
);
});
describe('parseParallelismOption', () => {
it('coerces false no parallelism', () => {
expect(parseParallelismOption(false)).toBe(1);
expect(parseParallelismOption('false')).toBe(1);
});
describe('isParallelDefault', () => {
it('returns true if default value', () => {
expect(isParallelDefault(undefined)).toBeTruthy();
expect(isParallelDefault(true)).toBeTruthy();
});
it('coerces true or undefined to default parallelism', () => {
expect(parseParallelismOption(true)).toBe(4);
expect(parseParallelismOption('true')).toBe(4);
expect(parseParallelismOption(undefined)).toBe(4);
expect(parseParallelismOption(null)).toBe(4);
});
it('returns false if not default value', () => {
expect(isParallelDefault(false)).toBeFalsy();
expect(isParallelDefault(2)).toBeFalsy();
expect(isParallelDefault('true' as any)).toBeFalsy();
});
it('coerces number string to number', () => {
expect(parseParallelismOption('2')).toBe(2);
});
it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => {
expect(() => parseParallelismOption(value as any)).toThrowError(
`Parallel option value '${value}' is not a boolean or integer`,
);
});
});
describe('getEnvironmentParallelism', () => {
it('reads the parallelism setting from the environment', () => {
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2';
expect(getEnvironmentParallelism()).toBe(2);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true';
expect(getEnvironmentParallelism()).toBe(4);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false';
expect(getEnvironmentParallelism()).toBe(1);
delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL;
expect(getEnvironmentParallelism()).toBe(4);
});
});
describe('runParallelWorkers', () => {
it('executes work in parallel', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismFactor: 0.5, // 2 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
expect(done).toEqual([0]);
waiting[1]();
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
expect(done).toEqual([0, 1, 2]);
waiting[3]();
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
it('executes work sequentially', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismFactor: 0, // 1 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([0]);
waiting[1]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3]);
waiting[3]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
});
+57 -14
View File
@@ -14,30 +14,31 @@
* limitations under the License.
*/
export const DEFAULT_PARALLELISM = 4;
export const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL';
export type ParallelOption = boolean | number | undefined;
export type ParallelismOption = boolean | string | number | null | undefined;
export function isParallelDefault(parallel: ParallelOption) {
return parallel === undefined || parallel === true;
}
export function parseParallel(
parallel: boolean | string | number | undefined,
): ParallelOption {
export function parseParallelismOption(parallel: ParallelismOption): number {
if (parallel === undefined || parallel === null) {
return true;
return DEFAULT_PARALLELISM;
} else if (typeof parallel === 'boolean') {
return parallel;
return parallel ? DEFAULT_PARALLELISM : 1;
} else if (typeof parallel === 'number' && Number.isInteger(parallel)) {
if (parallel < 1) {
return 1;
}
return parallel;
} else if (typeof parallel === 'string') {
if (parallel === 'true') {
return true;
return parseParallelismOption(true);
} else if (parallel === 'false') {
return false;
} else if (Number.isInteger(parseFloat(parallel.toString()))) {
return Number(parallel);
return parseParallelismOption(false);
}
const parsed = Number(parallel);
if (Number.isInteger(parsed)) {
return parseParallelismOption(parsed);
}
}
@@ -45,3 +46,45 @@ export function parseParallel(
`Parallel option value '${parallel}' is not a boolean or integer`,
);
}
export function getEnvironmentParallelism() {
return parseParallelismOption(process.env[PARALLEL_ENV_VAR]);
}
type ParallelWorkerOptions<TItem> = {
/**
* Decides the number of parallel workers by multiplying
* this with the configured parallelism, which defaults to 4.
*
* Defaults to 1.
*/
parallelismFactor?: number;
parallelismSetting?: ParallelismOption;
items: Iterable<TItem>;
worker: (item: TItem) => Promise<void>;
};
export async function runParallelWorkers<TItem>(
options: ParallelWorkerOptions<TItem>,
) {
const { parallelismFactor = 1, parallelismSetting, items, worker } = options;
const parallelism = parallelismSetting
? parseParallelismOption(parallelismSetting)
: getEnvironmentParallelism();
const sharedIterator = items[Symbol.iterator]();
const sharedIterable = {
[Symbol.iterator]: () => sharedIterator,
};
const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1);
return Promise.all(
Array(workerCount)
.fill(0)
.map(async () => {
for (const value of sharedIterable) {
await worker(value);
}
}),
);
}
+1 -1
View File
@@ -28,7 +28,7 @@ const DEP_TYPES = [
// Package data as returned by `yarn info`
export type YarnInfoInspectData = {
name: string;
'dist-tags': { latest: string };
'dist-tags': Record<string, string>;
versions: string[];
time: { [version: string]: string };
};
+7
View File
@@ -1,5 +1,12 @@
# @backstage/codemods
## 0.1.33-next.0
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.8-next.0
## 0.1.32
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
"version": "0.1.32",
"version": "0.1.33-next.0",
"private": false,
"publishConfig": {
"access": "public",
+1 -1
View File
@@ -45,7 +45,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+8
View File
@@ -1,5 +1,13 @@
# @backstage/core-components
## 0.8.8-next.0
### Patch Changes
- 8d785a0b1b: chore: bump `ansi-regex` from `5.0.1` to `6.0.1`
- f2dfbd3fb0: Adjust ErrorPage to accept optional supportUrl property to override app support config. Update type of additionalInfo property to be ReactNode to accept both string and component.
- d62bdb7a8e: The `ErrorPage` now falls back to using the default support configuration if the `ConfigApi` is not available.
## 0.8.7
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
"version": "0.8.7",
"version": "0.8.8-next.0",
"private": false,
"publishConfig": {
"access": "public",
@@ -74,7 +74,7 @@
},
"devDependencies": {
"@backstage/core-app-api": "^0.5.2",
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
@@ -16,6 +16,7 @@
import React from 'react';
import { ErrorPage } from './ErrorPage';
import { Link } from '../../components/Link';
import { renderInTestApp } from '@backstage/test-utils';
describe('<ErrorPage/>', () => {
@@ -30,4 +31,72 @@ describe('<ErrorPage/>', () => {
).toBeInTheDocument();
expect(getByTestId('go-back-link')).toBeInTheDocument();
});
it('should render with additional information of type string', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
additionalInfo="This is a string based additional information"
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(
getByText(/This is a string based additional information/i),
).toBeInTheDocument();
});
it('should render with additional information including link', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
additionalInfo={
<>
This is some additional information including{' '}
<Link to="/test">a link</Link>
</>
}
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/a link/i)).toBeInTheDocument();
expect(getByText(/a link/i)).toHaveAttribute('href', '/test');
});
it('should render with default support url if supportUrl is not provided', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/contact support/i)).toBeInTheDocument();
expect(getByText(/contact support/i)).toHaveAttribute(
'href',
'https://github.com/backstage/backstage/issues',
);
});
it('should override support url if supportUrl property is provided', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
supportUrl="https://error-page-test-support-url.com"
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/contact support/i)).toBeInTheDocument();
expect(getByText(/contact support/i)).toHaveAttribute(
'href',
'https://error-page-test-support-url.com',
);
});
});
@@ -27,7 +27,8 @@ import { MicDrop } from './MicDrop';
interface IErrorPageProps {
status: string;
statusMessage: string;
additionalInfo?: string;
additionalInfo?: React.ReactNode;
supportUrl?: string;
}
/** @public */
@@ -62,7 +63,7 @@ const useStyles = makeStyles<BackstageTheme>(
*
*/
export function ErrorPage(props: IErrorPageProps) {
const { status, statusMessage, additionalInfo } = props;
const { status, statusMessage, additionalInfo, supportUrl } = props;
const classes = useStyles();
const navigate = useNavigate();
const support = useSupportConfig();
@@ -88,7 +89,8 @@ export function ErrorPage(props: IErrorPageProps) {
<Link to="#" data-testid="go-back-link" onClick={() => navigate(-1)}>
Go back
</Link>
... or please <Link to={support.url}>contact support</Link> if you
... or please{' '}
<Link to={supportUrl || support.url}>contact support</Link> if you
think this is a bug.
</Typography>
</Grid>
+1 -1
View File
@@ -43,7 +43,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/core-app-api": "^0.5.2-next.0",
"@backstage/test-utils": "^0.2.4-next.0",
"@testing-library/jest-dom": "^5.10.1",
+70
View File
@@ -1,5 +1,75 @@
# @backstage/create-app
## 0.4.19-next.0
### Patch Changes
- 22f4ecb0e6: Switched the `file:` dependency for a `link:` dependency in the `backend` package. This makes sure that the `app` package is linked in rather than copied.
To apply this update to an existing app, make the following change to `packages/backend/package.json`:
```diff
"dependencies": {
- "app": "file:../app",
+ "app": "link:../app",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
```
- 1dd5a02e91: **BREAKING:** Updated `knex` to major version 1, which also implies changing out
the underlying `sqlite` implementation.
The old `sqlite3` NPM library has been abandoned by its maintainers, which has
led to unhandled security reports and other issues. Therefore, in the `knex` 1.x
release line they have instead switched over to the [`@vscode/sqlite3`
library](https://github.com/microsoft/vscode-node-sqlite3) by default, which is
actively maintained by Microsoft.
This means that as you update to this version of Backstage, there are two
breaking changes that you will have to address in your own repository:
## Bumping `knex` itself
All `package.json` files of your repo that used to depend on a 0.x version of
`knex`, should now be updated to depend on the 1.x release line. This applies in
particular to `packages/backend`, but may also occur in backend plugins or
libraries.
```diff
- "knex": "^0.95.1",
+ "knex": "^1.0.2",
```
Almost all existing database code will continue to function without modification
after this bump. The only significant difference that we discovered in the main
repo, is that the `alter()` function had a slightly different signature in
migration files. It now accepts an object with `alterType` and `alterNullable`
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
information about the `alter` syntax.
Also see the [`knex` changelog](https://knexjs.org/#changelog) for information
about breaking changes in the 1.x line; if you are using `RETURNING` you may
want to make some additional modifications in your code.
## Switching out `sqlite3`
All `package.json` files of your repo that used to depend on `sqlite3`, should
now be updated to depend on `@vscode/sqlite3`. This applies in particular to
`packages/backend`, but may also occur in backend plugins or libraries.
```diff
- "sqlite3": "^5.0.1",
+ "@vscode/sqlite3": "^5.0.7",
```
These should be functionally equivalent, except that the new library will have
addressed some long standing problems with old transitive dependencies etc.
## 0.4.18
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
"version": "0.4.18",
"version": "0.4.19-next.0",
"private": false,
"publishConfig": {
"access": "public"
+1 -1
View File
@@ -208,6 +208,6 @@ describe('templatingTask', () => {
// backend dependencies include `sqlite3` from `context.SQLite`
expect(
fs.readFileSync('templatedApp/packages/backend/package.json', 'utf-8'),
).toContain('"sqlite3"');
).toContain('sqlite3"');
});
});
@@ -40,7 +40,7 @@
"pg": "^8.3.0",
{{/if}}
{{#if dbTypeSqlite}}
"sqlite3": "^5.0.1",
"@vscode/sqlite3": "^5.0.7",
{{/if}}
"winston": "^3.2.1"
},
+10
View File
@@ -1,5 +1,15 @@
# @backstage/dev-utils
## 0.2.21-next.0
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.8-next.0
- @backstage/plugin-catalog-react@0.6.14-next.0
- @backstage/app-defaults@0.1.7-next.0
- @backstage/integration-react@0.1.21-next.0
## 0.2.20
### Patch Changes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.2.20",
"version": "0.2.21-next.0",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,13 +29,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/app-defaults": "^0.1.6",
"@backstage/app-defaults": "^0.1.7-next.0",
"@backstage/core-app-api": "^0.5.2",
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/catalog-model": "^0.9.10",
"@backstage/integration-react": "^0.1.20",
"@backstage/plugin-catalog-react": "^0.6.13",
"@backstage/integration-react": "^0.1.21-next.0",
"@backstage/plugin-catalog-react": "^0.6.14-next.0",
"@backstage/test-utils": "^0.2.4",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
@@ -55,7 +55,7 @@
"react-dom": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
+1 -1
View File
@@ -35,7 +35,7 @@
"serialize-error": "^8.0.1"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0",
"@backstage/cli": "^0.13.2-next.0",
"@types/jest": "^26.0.7"
},
"files": [
+7
View File
@@ -1,5 +1,12 @@
# @backstage/integration-react
## 0.1.21-next.0
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.8.8-next.0
## 0.1.20
### Patch Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
"version": "0.1.20",
"version": "0.1.21-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -22,7 +22,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.13",
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/integration": "^0.7.2",
"@backstage/theme": "^0.2.14",
@@ -35,8 +35,8 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/dev-utils": "^0.2.20",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/dev-utils": "^0.2.21-next.0",
"@backstage/test-utils": "^0.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+1 -1
View File
@@ -39,7 +39,7 @@
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/config-loader": "^0.9.3",
"@backstage/test-utils": "^0.2.4-next.0",
"@types/jest": "^26.0.7",
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+1
View File
@@ -0,0 +1 @@
# @backstage/release-manifests
+3
View File
@@ -0,0 +1,3 @@
# @backstage/release-manifests
This package provides a mapping between a Backstage release and the packages included in that release.
+34
View File
@@ -0,0 +1,34 @@
## API Report File for "@backstage/release-manifests"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public
export function getManifestByReleaseLine(
options: GetManifestByReleaseLineOptions,
): Promise<ReleaseManifest>;
// @public
export type GetManifestByReleaseLineOptions = {
releaseLine: string;
};
// @public
export function getManifestByVersion(
options: GetManifestByVersionOptions,
): Promise<ReleaseManifest>;
// @public
export type GetManifestByVersionOptions = {
version: string;
};
// @public
export type ReleaseManifest = {
releaseVersion: string;
packages: {
name: string;
version: string;
}[];
};
```
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@backstage/release-manifests",
"description": "Helper library for receiving release manifests",
"version": "0.0.0",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/release-manifests"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/test-utils": "^0.2.3",
"msw": "^0.35.0",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32"
},
"files": [
"dist"
]
}
+28
View File
@@ -0,0 +1,28 @@
/*
* 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.
*/
/**
* Contains mapping between Backstage release and package versions.
*
* @packageDocumentation
*/
export { getManifestByVersion, getManifestByReleaseLine } from './manifest';
export type {
ReleaseManifest,
GetManifestByReleaseLineOptions,
GetManifestByVersionOptions,
} from './manifest';
@@ -0,0 +1,86 @@
/*
* 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 { setupRequestMockHandlers } from '@backstage/test-utils';
import { getManifestByReleaseLine, getManifestByVersion } from './manifest';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
describe('getManifestByVersion', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
it('should return a list of packages in a release', async () => {
worker.use(
rest.get('*/v1/releases/0.0.0/manifest.json', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [{ name: '@backstage/core', version: '1.2.3' }],
}),
),
),
rest.get('*/v1/releases/999.0.1/manifest.json', (_, res, ctx) =>
res(ctx.status(404), ctx.json({})),
),
);
const pkgs = await getManifestByVersion({ version: '0.0.0' });
expect(pkgs.packages).toEqual([
{
name: '@backstage/core',
version: '1.2.3',
},
]);
await expect(getManifestByVersion({ version: '999.0.1' })).rejects.toThrow(
'No release found for 999.0.1 version',
);
});
});
describe('getManifestByReleaseLine', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
it('should return a list of packages in a release', async () => {
worker.use(
rest.get('*/v1/tags/main/manifest.json', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
packages: [{ name: '@backstage/core', version: '1.2.3' }],
}),
),
),
rest.get('*/v1/tags/foo/manifest.json', (_, res, ctx) =>
res(ctx.status(404), ctx.json({})),
),
);
const pkgs = await getManifestByReleaseLine({ releaseLine: 'main' });
expect(pkgs.packages).toEqual([
{
name: '@backstage/core',
version: '1.2.3',
},
]);
await expect(
getManifestByReleaseLine({ releaseLine: 'foo' }),
).rejects.toThrow("No 'foo' release line found");
});
});
@@ -0,0 +1,88 @@
/*
* 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 fetch from 'cross-fetch';
const VERSIONS_DOMAIN = 'https://versions.backstage.io';
/**
* Contains mapping between Backstage release and package versions.
* @public
*/
export type ReleaseManifest = {
releaseVersion: string;
packages: { name: string; version: string }[];
};
/**
* Options for {@link getManifestByVersion}.
* @public
*/
export type GetManifestByVersionOptions = {
version: string;
};
/**
* Returns a release manifest based on supplied version.
* @public
*/
export async function getManifestByVersion(
options: GetManifestByVersionOptions,
): Promise<ReleaseManifest> {
const url = `${VERSIONS_DOMAIN}/v1/releases/${encodeURIComponent(
options.version,
)}/manifest.json`;
const response = await fetch(url);
if (response.status === 404) {
throw new Error(`No release found for ${options.version} version`);
}
if (response.status !== 200) {
throw new Error(
`Unexpected response status ${response.status} when fetching release from ${url}.`,
);
}
return await response.json();
}
/**
* Options for {@link getManifestByReleaseLine}.
* @public
*/
export type GetManifestByReleaseLineOptions = {
releaseLine: string;
};
/**
* Returns a release manifest based on supplied release line.
* @public
*/
export async function getManifestByReleaseLine(
options: GetManifestByReleaseLineOptions,
): Promise<ReleaseManifest> {
const url = `${VERSIONS_DOMAIN}/v1/tags/${encodeURIComponent(
options.releaseLine,
)}/manifest.json`;
const response = await fetch(url);
if (response.status === 404) {
throw new Error(`No '${options.releaseLine}' release line found`);
}
if (response.status !== 200) {
throw new Error(
`Unexpected response status ${response.status} when fetching release from ${url}.`,
);
}
return await response.json();
}
+1 -1
View File
@@ -40,7 +40,7 @@
"@backstage/plugin-permission-common": "^0.4.0-next.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.1-next.0"
"@backstage/cli": "^0.13.2-next.0"
},
"jest": {
"roots": [
@@ -1,5 +1,17 @@
# techdocs-cli-embedded-app
## 0.2.63-next.0
### Patch Changes
- Updated dependencies
- @backstage/cli@0.13.2-next.0
- @backstage/core-components@0.8.8-next.0
- @backstage/app-defaults@0.1.7-next.0
- @backstage/integration-react@0.1.21-next.0
- @backstage/plugin-catalog@0.7.12-next.0
- @backstage/plugin-techdocs@0.13.3-next.0
## 0.2.62
### Patch Changes
@@ -1,12 +0,0 @@
# NOTE: This file is used for testing techdocs-cli locally
app:
title: Techdocs Preview App
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
techdocs:
builder: 'external'
requestUrl: http://localhost:7007/api
@@ -1,19 +1,19 @@
{
"name": "techdocs-cli-embedded-app",
"version": "0.2.62",
"version": "0.2.63-next.0",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/app-defaults": "^0.1.6",
"@backstage/app-defaults": "^0.1.7-next.0",
"@backstage/catalog-model": "^0.9.10",
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@backstage/config": "^0.1.13",
"@backstage/core-app-api": "^0.5.2",
"@backstage/core-components": "^0.8.7",
"@backstage/core-components": "^0.8.8-next.0",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/integration-react": "^0.1.20",
"@backstage/plugin-catalog": "^0.7.11",
"@backstage/plugin-techdocs": "^0.13.2",
"@backstage/integration-react": "^0.1.21-next.0",
"@backstage/plugin-catalog": "^0.7.12-next.0",
"@backstage/plugin-techdocs": "^0.13.3-next.0",
"@backstage/test-utils": "^0.2.4",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.11.0",
@@ -26,7 +26,7 @@
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "^0.13.1",
"@backstage/cli": "^0.13.2-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
@@ -39,9 +39,8 @@
"start-server-and-test": "^1.10.11"
},
"scripts": {
"start": "backstage-cli app:serve --config ./app-config.yaml --config ./app-config.dev.yaml",
"start": "backstage-cli app:serve --config ./app-config.yaml",
"build": "backstage-cli app:build --config ./app-config.yaml",
"build:dev": "backstage-cli app:build --config ./app-config.dev.yaml",
"clean": "backstage-cli clean",
"test": "backstage-cli test",
"lint": "backstage-cli lint",

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