diff --git a/.changeset/afraid-boats-check.md b/.changeset/afraid-boats-check.md deleted file mode 100644 index d36fda4e03..0000000000 --- a/.changeset/afraid-boats-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Specify type of `visibilityPermission` property on collators and collator factories. diff --git a/.changeset/angry-pens-begin.md b/.changeset/angry-pens-begin.md deleted file mode 100644 index ed5bbf4c4e..0000000000 --- a/.changeset/angry-pens-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Override default commit message and author details in GitLab action diff --git a/.changeset/beige-avocados-matter.md b/.changeset/beige-avocados-matter.md deleted file mode 100644 index d6c1e3b2a5..0000000000 --- a/.changeset/beige-avocados-matter.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**BREAKING (alpha api):** Replace `createCatalogPolicyDecision` export with `createCatalogConditionalDecision`, which accepts a permission parameter of type `ResourcePermission<'catalog-entity'>` along with conditions. The permission passed is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: - -```typescript -class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - _user?: BackstageIdentityResponse, - ): Promise { - if ( - // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> - isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) - ) { - return createCatalogConditionalDecision( - request.permission, - catalogConditions.isEntityOwner( - _user?.identity.ownershipEntityRefs ?? [], - ), - ); - } - - return { - result: AuthorizeResult.ALLOW, - }; -``` diff --git a/.changeset/beige-baboons-walk.md b/.changeset/beige-baboons-walk.md deleted file mode 100644 index a5b424a489..0000000000 --- a/.changeset/beige-baboons-walk.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/integration': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` diff --git a/.changeset/beige-panthers-dream.md b/.changeset/beige-panthers-dream.md new file mode 100644 index 0000000000..e477419c13 --- /dev/null +++ b/.changeset/beige-panthers-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixed a bug that was introduced in `0.13.1-next.0` which caused the `ent` claim of issued tokens to be dropped. diff --git a/.changeset/big-buses-clap.md b/.changeset/big-buses-clap.md deleted file mode 100644 index 503aa314bc..0000000000 --- a/.changeset/big-buses-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-bazaar': patch ---- - -Pass authorization header with Backstage token to backend requests. diff --git a/.changeset/big-mayflies-sin.md b/.changeset/big-mayflies-sin.md deleted file mode 100644 index 6aa18fc9c5..0000000000 --- a/.changeset/big-mayflies-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend': patch ---- - -Check for non-resource permissions when authorizing result-by-result in AuthorizedSearchEngine. diff --git a/.changeset/blue-beers-kiss.md b/.changeset/blue-beers-kiss.md deleted file mode 100644 index 15ea58efb4..0000000000 --- a/.changeset/blue-beers-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-react': minor ---- - -**BREAKING**: More restrictive typing for `usePermission` hook and `PermissionedRoute` component. It's no longer possible to pass a `resourceRef` unless the permission is of type `ResourcePermission`. diff --git a/.changeset/blue-geese-work.md b/.changeset/blue-geese-work.md deleted file mode 100644 index b40c1f93b6..0000000000 --- a/.changeset/blue-geese-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Updated openid-client from 4.1.2 to 5.1.3 diff --git a/.changeset/brave-chairs-stare.md b/.changeset/brave-chairs-stare.md deleted file mode 100644 index 8a2dd91e91..0000000000 --- a/.changeset/brave-chairs-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-tasks': patch ---- - -Refactored the internal `TaskWorker` class to make it easier to test. diff --git a/.changeset/breezy-days-prove.md b/.changeset/breezy-days-prove.md new file mode 100644 index 0000000000..d3bc3c169e --- /dev/null +++ b/.changeset/breezy-days-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Add new bitbucket(Cloud/Server) integrations to ScaffolderClient. diff --git a/.changeset/breezy-mugs-build.md b/.changeset/breezy-mugs-build.md deleted file mode 100644 index 026932e971..0000000000 --- a/.changeset/breezy-mugs-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package. diff --git a/.changeset/breezy-schools-compete.md b/.changeset/breezy-schools-compete.md deleted file mode 100644 index b6e82eff25..0000000000 --- a/.changeset/breezy-schools-compete.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Fixed an issue where sometimes entities would have stale relations "stuck" and -not getting removed as expected, after the other end of the relation had stopped -referring to them. diff --git a/.changeset/slimy-horses-do.md b/.changeset/bright-panthers-guess.md similarity index 51% rename from .changeset/slimy-horses-do.md rename to .changeset/bright-panthers-guess.md index 196c47c988..a83f2e4bf1 100644 --- a/.changeset/slimy-horses-do.md +++ b/.changeset/bright-panthers-guess.md @@ -2,4 +2,4 @@ '@backstage/plugin-api-docs': patch --- -Add dedicated gRPC api definition widget +Updated dependency `graphiql` to `^1.8.8`. diff --git a/.changeset/calm-walls-heal.md b/.changeset/calm-walls-heal.md deleted file mode 100644 index f47ce4a0e9..0000000000 --- a/.changeset/calm-walls-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': minor ---- - -Add `resourceType` property to `PermissionCondition` type to allow matching them with `ResourcePermission` instances. diff --git a/.changeset/chatty-forks-bathe.md b/.changeset/chatty-forks-bathe.md new file mode 100644 index 0000000000..e6d3816cc6 --- /dev/null +++ b/.changeset/chatty-forks-bathe.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-adr': minor +'@backstage/plugin-adr-backend': minor +'@backstage/plugin-adr-common': minor +--- + +Implement ADR plugin diff --git a/.changeset/clean-lions-taste.md b/.changeset/clean-lions-taste.md deleted file mode 100644 index 1ec1eec952..0000000000 --- a/.changeset/clean-lions-taste.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Made `User` and `Group` entity kinds not permitted by the default -`catalog.rules` config. - -The effect of this is that after creating a new Backstage repository, its -catalog no longer permits regular users to register `User` or `Group` entities -using the Backstage interface. Additionally, if you have config locations that -result in `User` or `Group` entities, you need to add those kinds to its own -specific rules: - -```yaml -catalog: - locations: - # This applies for example to url type locations - - type: url - target: https://example.com/org.yaml - rules: - - allow: [User, Group] - # But also note that this applies to ALL org location types! - - type: github-org - target: https://github.com/my-org-name - rules: - - allow: [User, Group] -``` - -This rule change does NOT affect entity providers, only things that are emitted -by entity processors. - -We recommend that this change is applied to your own Backstage repository, since -it makes it impossible for regular end users to affect your org data through -e.g. YAML files. To do so, remove the two kinds from the default rules in your config: - -```diff - catalog: - rules: -- - allow: [Component, System, API, Group, User, Resource, Location] -+ - allow: [Component, System, API, Resource, Location] -``` - -And for any location that in any way results in org data being ingested, add the corresponding rule to it: - -```diff - catalog: - locations: - - type: github-org - target: https://github.com/my-org-name -+ rules: -+ - allow: [User, Group] -``` diff --git a/.changeset/clever-buckets-doubt.md b/.changeset/clever-buckets-doubt.md deleted file mode 100644 index 751db72bf8..0000000000 --- a/.changeset/clever-buckets-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search` diff --git a/.changeset/clever-donuts-teach.md b/.changeset/clever-donuts-teach.md deleted file mode 100644 index 2304de1137..0000000000 --- a/.changeset/clever-donuts-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Prevent permissions with types other than `ResourcePermission<'catalog-entity'>` from being used with the `useEntityPermission` hook. diff --git a/.changeset/clever-pumpkins-tease.md b/.changeset/clever-pumpkins-tease.md deleted file mode 100644 index d0784361ff..0000000000 --- a/.changeset/clever-pumpkins-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Bump the `rushstack` api generator libraries to their latest versions diff --git a/.changeset/shiny-seas-yell.md b/.changeset/clever-trains-greet.md similarity index 51% rename from .changeset/shiny-seas-yell.md rename to .changeset/clever-trains-greet.md index 9a939701d8..dc75f2f02a 100644 --- a/.changeset/shiny-seas-yell.md +++ b/.changeset/clever-trains-greet.md @@ -2,9 +2,10 @@ '@backstage/plugin-tech-insights-backend': minor --- -This backend now uses the `@backstage/backend-tasks` package facilities for scheduling fact retrievers. - -**BREAKING**: The `buildTechInsightsContext` function now takes an additional field in its options argument: `scheduler`. This is an instance of `PluginTaskScheduler`, which can be found in your backend initialization code's `env`. +**BREAKING**: The `buildTechInsightsContext` function now takes an additional +field in its options argument: `tokenManager`. This is an instance of +`TokenManager`, which can be found in your backend initialization code's +`env`. ```diff const builder = buildTechInsightsContext({ @@ -12,7 +13,8 @@ This backend now uses the `@backstage/backend-tasks` package facilities for sche config: env.config, database: env.database, discovery: env.discovery, -+ scheduler: env.scheduler, + scheduler: env.scheduler, ++ tokenManager: env.tokenManager, factRetrievers: [ /* ... */ ], }); ``` diff --git a/.changeset/cold-steaks-provide.md b/.changeset/cold-steaks-provide.md deleted file mode 100644 index 8a107f7b53..0000000000 --- a/.changeset/cold-steaks-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -Added a context menu to the scaffolder page that provides links to the template editor and actions reference. These links and the presence of the context menu can be toggled through the `contextMenu` prop of the scaffolder page. diff --git a/.changeset/cool-mice-sit.md b/.changeset/cool-mice-sit.md new file mode 100644 index 0000000000..321b5fcf0b --- /dev/null +++ b/.changeset/cool-mice-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights': patch +--- + +Add new component `EntityTechInsightsScorecardCard`, which can be used in the overview of the `EntityPage` page or display multiple individual `EntityTechInsightsScorecardCard` in `EntityLayout.Route`. diff --git a/.changeset/cool-ties-share.md b/.changeset/cool-ties-share.md new file mode 100644 index 0000000000..fb76785fcf --- /dev/null +++ b/.changeset/cool-ties-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Gerrit integration: Added optional `cloneUrl` string to config. diff --git a/.changeset/cuddly-turtles-sleep.md b/.changeset/cuddly-turtles-sleep.md deleted file mode 100644 index a63cd44232..0000000000 --- a/.changeset/cuddly-turtles-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions. diff --git a/.changeset/curly-parrots-applaud.md b/.changeset/curly-parrots-applaud.md new file mode 100644 index 0000000000..d9443415eb --- /dev/null +++ b/.changeset/curly-parrots-applaud.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Update the `github:publish` action to allow passing required status check +contexts before merging to the main branch. diff --git a/.changeset/dependabot-05b19b9.md b/.changeset/dependabot-05b19b9.md deleted file mode 100644 index 97845bb77d..0000000000 --- a/.changeset/dependabot-05b19b9.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `eslint-plugin-jest` from 25.3.4 to 26.1.2 diff --git a/.changeset/dependabot-2b68456.md b/.changeset/dependabot-2b68456.md deleted file mode 100644 index f54dd17d55..0000000000 --- a/.changeset/dependabot-2b68456.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/catalog-model': patch -'@backstage/config-loader': patch -'@backstage/plugin-tech-insights-backend-module-jsonfc': patch ---- - -build(deps): bump `ajv` from 7.0.3 to 8.10.0 diff --git a/.changeset/dependabot-456f3f4.md b/.changeset/dependabot-456f3f4.md deleted file mode 100644 index 1ccd243715..0000000000 --- a/.changeset/dependabot-456f3f4.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -build(deps): bump `cronstrue` from 1.125.0 to 2.2.0 diff --git a/.changeset/dependabot-51da438.md b/.changeset/dependabot-51da438.md deleted file mode 100644 index d7d735b9a1..0000000000 --- a/.changeset/dependabot-51da438.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -build(deps): bump `isbinaryfile` from 4.0.8 to 5.0.0 diff --git a/.changeset/dependabot-6754062.md b/.changeset/dependabot-6754062.md deleted file mode 100644 index 14d9648071..0000000000 --- a/.changeset/dependabot-6754062.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `eslint-webpack-plugin` from 2.6.0 to 3.1.1 diff --git a/.changeset/dependabot-699dc23.md b/.changeset/dependabot-699dc23.md deleted file mode 100644 index b73b0eab46..0000000000 --- a/.changeset/dependabot-699dc23.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kafka-backend': patch ---- - -build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0 diff --git a/.changeset/dependabot-7272684.md b/.changeset/dependabot-7272684.md deleted file mode 100644 index 6f68191ab7..0000000000 --- a/.changeset/dependabot-7272684.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -build(deps): bump `@gitbeaker/core` from 34.6.0 to 35.6.0 diff --git a/.changeset/dependabot-84caed8.md b/.changeset/dependabot-84caed8.md deleted file mode 100644 index d4806431a0..0000000000 --- a/.changeset/dependabot-84caed8.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `@spotify/eslint-config-base` from 12.0.0 to 13.0.0 diff --git a/.changeset/dependabot-a13a4a3.md b/.changeset/dependabot-a13a4a3.md deleted file mode 100644 index 5a339330e7..0000000000 --- a/.changeset/dependabot-a13a4a3.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -'@backstage/core-app-api': patch -'@backstage/core-components': patch -'@backstage/core-plugin-api': patch -'@backstage/dev-utils': patch -'@backstage/integration-react': patch -'@backstage/test-utils': patch -'@backstage/plugin-airbrake': patch -'@backstage/plugin-allure': patch -'@backstage/plugin-analytics-module-ga': patch -'@backstage/plugin-apache-airflow': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-badges': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-code-climate': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore-react': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-firehydrant': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-gcalendar': patch -'@backstage/plugin-gcp-projects': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-gocd': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-home': patch -'@backstage/plugin-ilert': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-newrelic': patch -'@backstage/plugin-org': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-periskop': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-shortcuts': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-stack-overflow': patch -'@backstage/plugin-tech-insights': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-todo': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-xcmetrics': patch ---- - -build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 diff --git a/.changeset/dependabot-c1fe92f.md b/.changeset/dependabot-c1fe92f.md deleted file mode 100644 index 8d106e3962..0000000000 --- a/.changeset/dependabot-c1fe92f.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `@spotify/eslint-config-typescript` diff --git a/.changeset/dependabot-c762551.md b/.changeset/dependabot-c762551.md deleted file mode 100644 index 4da9ddd18d..0000000000 --- a/.changeset/dependabot-c762551.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `@spotify/eslint-config-react` from 12.0.0 to 13.0.0 diff --git a/.changeset/dependabot-cccf2f0.md b/.changeset/dependabot-cccf2f0.md deleted file mode 100644 index 6b420f9a48..0000000000 --- a/.changeset/dependabot-cccf2f0.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0 diff --git a/.changeset/dependabot-ebb092c.md b/.changeset/dependabot-ebb092c.md deleted file mode 100644 index 78850d2065..0000000000 --- a/.changeset/dependabot-ebb092c.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -build(deps): bump `@asyncapi/react-component` diff --git a/.changeset/dirty-eagles-hunt.md b/.changeset/dirty-eagles-hunt.md new file mode 100644 index 0000000000..d3ad27dfc3 --- /dev/null +++ b/.changeset/dirty-eagles-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Split BitbucketUrlReader into BitbucketCloudUrlReader and BitbucketServerUrlReader. Backwards compatible. diff --git a/.changeset/dry-fans-arrive.md b/.changeset/dry-fans-arrive.md deleted file mode 100644 index 6947703e7f..0000000000 --- a/.changeset/dry-fans-arrive.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -'@backstage/plugin-permission-node': minor ---- - -**BREAKING**: Stronger typing in `PermissionPolicy` 🎉. - -Previously, it was entirely the responsibility of the `PermissionPolicy` author to only return `CONDITIONAL` decisions for permissions that are associated with a resource, and to return the correct kind of `PermissionCondition` instances inside the decision. Now, the policy authoring helpers provided in this package now ensure that the decision and permission match. - -**For policy authors**: rename and adjust api of `createConditionExports`. Previously, the function returned a factory for creating conditional decisions named `createPolicyDecision`, which had a couple of drawbacks: - -1. The function always creates a _conditional_ policy decision, but this was not reflected in the name. -2. Conditional decisions should only ever be returned from `PermissionPolicy#handle` for resource permissions, but there was nothing in the API that encoded this constraint. - -This change addresses the drawbacks above by making the following changes for policy authors: - -- The `createPolicyDecision` method has been renamed to `createConditionalDecision`. -- Along with conditions, the method now accepts a permission, which must be a `ResourcePermission`. This is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: - -```typescript -class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - _user?: BackstageIdentityResponse, - ): Promise { - if ( - // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> - isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) - ) { - return createCatalogConditionalDecision( - request.permission, - catalogConditions.isEntityOwner( - _user?.identity.ownershipEntityRefs ?? [], - ), - ); - } - - return { - result: AuthorizeResult.ALLOW, - }; -``` - -**BREAKING**: when creating `PermissionRule`s, provide a `resourceType`. - -```diff -export const isEntityOwner = createCatalogPermissionRule({ - name: 'IS_ENTITY_OWNER', - description: 'Allow entities owned by the current user', -+ resourceType: RESOURCE_TYPE_CATALOG_ENTITY, - apply: (resource: Entity, claims: string[]) => { - if (!resource.relations) { - return false; - } - - return resource.relations - .filter(relation => relation.type === RELATION_OWNED_BY) - .some(relation => claims.includes(relation.targetRef)); - }, - toQuery: (claims: string[]) => ({ - key: 'relations.ownedBy', - values: claims, - }), -}); -``` diff --git a/.changeset/eight-cobras-think.md b/.changeset/eight-cobras-think.md deleted file mode 100644 index af1c7cd113..0000000000 --- a/.changeset/eight-cobras-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend': patch ---- - -Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. diff --git a/.changeset/eighty-chicken-retire.md b/.changeset/eighty-chicken-retire.md new file mode 100644 index 0000000000..f6d66f5eba --- /dev/null +++ b/.changeset/eighty-chicken-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add filtering to GKE cluster locator diff --git a/.changeset/eleven-days-brush.md b/.changeset/eleven-days-brush.md deleted file mode 100644 index d628b068fc..0000000000 --- a/.changeset/eleven-days-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-tasks': minor ---- - -Adds the ability to manually trigger tasks which are registered diff --git a/.changeset/eleven-frogs-promise.md b/.changeset/eleven-frogs-promise.md deleted file mode 100644 index 1868bb9df5..0000000000 --- a/.changeset/eleven-frogs-promise.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-permission-common': minor ---- - -Refactor api types into more specific, decoupled names. - -- **BREAKING:** - - Renamed `AuthorizeDecision` to `EvaluatePermissionResponse` - - Renamed `AuthorizeQuery` to `EvaluatePermissionRequest` - - Renamed `AuthorizeRequest` to `EvaluatePermissionRequestBatch` - - Renamed `AuthorizeResponse` to `EvaluatePermissionResponseBatch` - - Renamed `Identified` to `IdentifiedPermissionMessage` -- Add `PermissionMessageBatch` helper type -- Add `ConditionalPolicyDecision`, `DefinitivePolicyDecision`, and `PolicyDecision` types from `@backstage/plugin-permission-node` diff --git a/.changeset/eleven-pens-collect.md b/.changeset/eleven-pens-collect.md deleted file mode 100644 index 0df1bb0400..0000000000 --- a/.changeset/eleven-pens-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Corrected replacements for depreciated FilteredEntityLayout items diff --git a/.changeset/empty-mugs-bow.md b/.changeset/empty-mugs-bow.md new file mode 100644 index 0000000000..d9d63e7f89 --- /dev/null +++ b/.changeset/empty-mugs-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +The authentication APIs are no longer `@alpha`. Since the `@backstage/core-plugin-api` has no `/alpha` entrypoint, the only effect of marking the APIs as `@alpha` was to hide them in documentation. They are still expected to be widely used and there will be a migration path if they are changed in the future. diff --git a/.changeset/empty-pens-invent.md b/.changeset/empty-pens-invent.md deleted file mode 100644 index d6937afc84..0000000000 --- a/.changeset/empty-pens-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend': minor ---- - -**BREAKING**: The `authorization` property is no longer returned on search results when queried. Note: this will only result in a breaking change if you have custom code in your frontend that relies on the `authorization.resourceRef` property on documents. diff --git a/.changeset/empty-poems-lick.md b/.changeset/empty-poems-lick.md new file mode 100644 index 0000000000..2f620758ed --- /dev/null +++ b/.changeset/empty-poems-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Update types to match the new version of `@keyv/redis` diff --git a/.changeset/fair-dingos-glow.md b/.changeset/fair-dingos-glow.md deleted file mode 100644 index 569f7a3fab..0000000000 --- a/.changeset/fair-dingos-glow.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Remove the `knex` package that is installed in the `packages/backend` as it's provided by the `@backstage/*` packages for you automatically. You can make the following change in your `packages/backend/package.json` if you wish to apply this change. - -```diff - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", - "clean": "backstage-cli package clean", -- "migrate:create": "knex migrate:make -x ts" -``` - -```diff - "express": "^4.17.1", - "express-promise-router": "^4.1.0", -- "knex": "^0.21.6", - "pg": "^8.3.0", -``` diff --git a/.changeset/fair-kings-retire.md b/.changeset/fair-kings-retire.md new file mode 100644 index 0000000000..156632f6ae --- /dev/null +++ b/.changeset/fair-kings-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Gerrit UrlReader: Implemented `readTree` diff --git a/.changeset/fair-lamps-leave.md b/.changeset/fair-lamps-leave.md deleted file mode 100644 index 9e902b51f2..0000000000 --- a/.changeset/fair-lamps-leave.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Wired up the OneLogin provider to be visible in the Settings UI when configured correctly. - -Previously it wasn't visible at all. diff --git a/.changeset/fair-pigs-mate.md b/.changeset/fair-pigs-mate.md new file mode 100644 index 0000000000..ce82e138b8 --- /dev/null +++ b/.changeset/fair-pigs-mate.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-tech-insights-node': minor +--- + +**BREAKING**: The `FactRetrieverContext` type now contains an additional +field: `tokenManager`. diff --git a/.changeset/famous-seas-flash.md b/.changeset/famous-seas-flash.md deleted file mode 100644 index cd19dc257e..0000000000 --- a/.changeset/famous-seas-flash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sentry': patch ---- - -Exposed options for the material table diff --git a/.changeset/fast-cheetahs-grow.md b/.changeset/fast-cheetahs-grow.md deleted file mode 100644 index 5567436b5e..0000000000 --- a/.changeset/fast-cheetahs-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-node': minor ---- - -**BREAKING**: Removed the deprecated `id` and `entity` fields from `BackstageSignInResult`. diff --git a/.changeset/few-buttons-breathe.md b/.changeset/few-buttons-breathe.md deleted file mode 100644 index 7c7996774a..0000000000 --- a/.changeset/few-buttons-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Add BackstageTab to overridableComponents so can override styles in a theme diff --git a/.changeset/few-hotels-approve.md b/.changeset/few-hotels-approve.md deleted file mode 100644 index 49ca040ef7..0000000000 --- a/.changeset/few-hotels-approve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Changed input label for owner field in GitlabRepoPicker diff --git a/.changeset/few-mayflies-divide.md b/.changeset/few-mayflies-divide.md deleted file mode 100644 index e8744c47da..0000000000 --- a/.changeset/few-mayflies-divide.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-permission-node': minor ---- - -**BREAKING:** - -- Rename `PolicyAuthorizeQuery` to `PolicyQuery` -- Remove `PolicyDecision`, `DefinitivePolicyDecision`, and `ConditionalPolicyDecision`. These types are now exported from `@backstage/plugin-permission-common` diff --git a/.changeset/few-seas-fail.md b/.changeset/few-seas-fail.md deleted file mode 100644 index c5d9db4d47..0000000000 --- a/.changeset/few-seas-fail.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-permission-common': patch ---- - -Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: - -- `authorize()`: Used when the caller requires a definitive decision. -- `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. diff --git a/.changeset/fluffy-grapes-sort.md b/.changeset/fluffy-grapes-sort.md deleted file mode 100644 index 461d80275d..0000000000 --- a/.changeset/fluffy-grapes-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Dynamically layout User Settings theme toggle container diff --git a/.changeset/forty-poets-tie.md b/.changeset/forty-poets-tie.md deleted file mode 100644 index 9118100934..0000000000 --- a/.changeset/forty-poets-tie.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Decouple tags picker from backend entities - -`EntityTagPicker` fetches all the tags independently and it doesn't require all the entities to be available client side. diff --git a/.changeset/forty-pumpkins-marry.md b/.changeset/forty-pumpkins-marry.md deleted file mode 100644 index 782fb5ae91..0000000000 --- a/.changeset/forty-pumpkins-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Now plugin configuration accept a new optional parameter `groupSelect` which allow the client to fetch defined fields from the ms-graph api. diff --git a/.changeset/four-birds-peel.md b/.changeset/four-birds-peel.md deleted file mode 100644 index e90f631858..0000000000 --- a/.changeset/four-birds-peel.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -- Adds new `HomePageStackOverflowQuestions` component which renders a list of stack overflow questions on your homepage. - -- Exports `ComponentRenderer` type. diff --git a/.changeset/four-dolphins-report.md b/.changeset/four-dolphins-report.md deleted file mode 100644 index fa3f06c532..0000000000 --- a/.changeset/four-dolphins-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-node': minor ---- - -**BREAKING:** `ServerPermissionClient` now implements `PermissionEvaluator`, which moves out the capabilities for evaluating conditional decisions from `authorize()` to `authorizeConditional()` method. diff --git a/.changeset/fresh-boxes-pull.md b/.changeset/fresh-boxes-pull.md deleted file mode 100644 index e73d7e25c2..0000000000 --- a/.changeset/fresh-boxes-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. diff --git a/.changeset/fresh-bulldogs-own.md b/.changeset/fresh-bulldogs-own.md deleted file mode 100644 index 7772262efe..0000000000 --- a/.changeset/fresh-bulldogs-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-badges-backend': patch ---- - -allow overriding `DefaultBadgeBuilder.getMarkdownCode` diff --git a/.changeset/fresh-cobras-admire.md b/.changeset/fresh-cobras-admire.md deleted file mode 100644 index c73d4ab5d5..0000000000 --- a/.changeset/fresh-cobras-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -export kubernetes components diff --git a/.changeset/fresh-dodos-rush.md b/.changeset/fresh-dodos-rush.md deleted file mode 100644 index b71c2ad055..0000000000 --- a/.changeset/fresh-dodos-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-insights-node': patch ---- - -Adds an optional timeout to fact retriever registrations to stop a task if it runs too long. diff --git a/.changeset/friendly-hairs-happen.md b/.changeset/friendly-hairs-happen.md deleted file mode 100644 index 219d5efbcd..0000000000 --- a/.changeset/friendly-hairs-happen.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -Add a new provider `AwsS3EntityProvider` as replacement for `AwsS3DiscoveryProcessor`. - -In order to migrate from the `AwsS3DiscoveryProcessor` you need to apply -the following changes: - -**Before:** - -```yaml -# app-config.yaml - -catalog: - locations: - - type: s3-discovery - target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/ -``` - -```ts -/* packages/backend/src/plugins/catalog.ts */ - -import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; - -const builder = await CatalogBuilder.create(env); -/** ... other processors ... */ -builder.addProcessor(new AwsS3DiscoveryProcessor(env.reader)); -``` - -**After:** - -```yaml -# app-config.yaml - -catalog: - providers: - awsS3: - yourProviderId: # identifies your dataset / provider independent of config changes - bucketName: sample-bucket - prefix: prefix/ # optional - region: us-east-2 # optional, uses the default region otherwise -``` - -```ts -/* packages/backend/src/plugins/catalog.ts */ - -import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; - -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -builder.addEntityProvider( - ...AwsS3EntityProvider.fromConfig(env.config, { - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ minutes: 30 }), - timeout: Duration.fromObject({ minutes: 3 }), - }), - }), -); -``` - -For simple setups, you can omit the provider ID at the config -which has the same effect as using `default` for it. - -```yaml -# app-config.yaml - -catalog: - providers: - awsS3: - # uses "default" as provider ID - bucketName: sample-bucket - prefix: prefix/ # optional - region: us-east-2 # optional, uses the default region otherwise -``` diff --git a/.changeset/fuzzy-seahorses-teach.md b/.changeset/fuzzy-seahorses-teach.md new file mode 100644 index 0000000000..71ef28dd52 --- /dev/null +++ b/.changeset/fuzzy-seahorses-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add instructions to `create-github-app` command. diff --git a/.changeset/giant-cheetahs-thank.md b/.changeset/giant-cheetahs-thank.md new file mode 100644 index 0000000000..a135fbc4a3 --- /dev/null +++ b/.changeset/giant-cheetahs-thank.md @@ -0,0 +1,30 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Add annotation `microsoft.com/email` when using the `defaultUserTransformer`. + +This will allow users of the Microsoft auth provider to utilize the predefined +SignIn resolver instead of maintaining their own. + +```typescript +// backend/plugins/auth.ts + +// [...] + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + // [...] + providerFactories: { + microsoft: providers.microsoft.create({ + signIn: { + resolver: + providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); +} +``` diff --git a/.changeset/gorgeous-donuts-float.md b/.changeset/gorgeous-donuts-float.md deleted file mode 100644 index 72a28ba6f9..0000000000 --- a/.changeset/gorgeous-donuts-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-todo-backend': patch ---- - -Fix method to get source-location. diff --git a/.changeset/seven-apricots-sell.md b/.changeset/grumpy-panthers-peel.md similarity index 53% rename from .changeset/seven-apricots-sell.md rename to .changeset/grumpy-panthers-peel.md index 977bdd7d49..ecf4397554 100644 --- a/.changeset/seven-apricots-sell.md +++ b/.changeset/grumpy-panthers-peel.md @@ -2,4 +2,4 @@ '@backstage/plugin-tech-insights-backend-module-jsonfc': patch --- -Removes node-cron from tech-insights to utilize backend-tasks +Updated usages of `buildTechInsightsContext` in README. diff --git a/.changeset/grumpy-parents-prove.md b/.changeset/grumpy-parents-prove.md deleted file mode 100644 index ff01fe13f0..0000000000 --- a/.changeset/grumpy-parents-prove.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Provides the ability to hide the relations toggle on the `OwnershipCard` as well as setting a default relation type. - -To hide the toggle simply include the `hideRelationsToggle` prop like this: - -```tsx - -``` - -To set the default relation type, add the `relationsType` prop with a value of direct or aggregated, the default if not provided is direct. Here is an example: - -```tsx - -``` diff --git a/.changeset/happy-foxes-arrive.md b/.changeset/happy-foxes-arrive.md deleted file mode 100644 index 53e59c8d77..0000000000 --- a/.changeset/happy-foxes-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Added an experimental `package fix` command which applies automated fixes to the target package. The initial fix that is available is to add missing monorepo dependencies to the target package. diff --git a/.changeset/happy-mugs-camp.md b/.changeset/happy-mugs-camp.md deleted file mode 100644 index 22d13d80d1..0000000000 --- a/.changeset/happy-mugs-camp.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-search-common': patch ---- - -- Introduce `SearchDocument` type. This type contains the subset of `IndexableDocument` properties relevant to the frontend, and is intended to be used for documents returned to the frontend from the search API. -- `SearchResultSet` is now a wrapper for documents of type `SearchDocument`, and is intended to be used in the frontend. This isn't a breaking change, since `IndexableDocument`s are valid `SearchDocument`s, so the old and new types are compatible. -- Introduce `IndexableResultSet` type, which wraps `IndexableDocument` instances in the same way as `SearchResultSet`. diff --git a/.changeset/heavy-stingrays-talk.md b/.changeset/heavy-stingrays-talk.md deleted file mode 100644 index d17a85090b..0000000000 --- a/.changeset/heavy-stingrays-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -The backend will no longer fail to start up when configured secrets do not match the configuration schema. diff --git a/.changeset/hip-adults-collect.md b/.changeset/hip-adults-collect.md new file mode 100644 index 0000000000..97afdf3da2 --- /dev/null +++ b/.changeset/hip-adults-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixing broken types for `knex` when checking returned rows diff --git a/.changeset/hip-poems-breathe.md b/.changeset/hip-poems-breathe.md deleted file mode 100644 index f8a507b2a7..0000000000 --- a/.changeset/hip-poems-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Stop logging "Stopped watcher" when shutting down the development backend. diff --git a/.changeset/honest-kids-live.md b/.changeset/honest-kids-live.md new file mode 100644 index 0000000000..e829d00d91 --- /dev/null +++ b/.changeset/honest-kids-live.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Optimized the command order in `packages/backend/Dockerfile` as well as added the `--no-install-recommends` to the `apt-get install` and tweaked the installed packages. + +To apply this change to an existing app, update your `packages/backend/Dockerfile` to match the documented `Dockerfile` at https://backstage.io/docs/deployment/docker#host-build. diff --git a/.changeset/hot-items-smoke.md b/.changeset/hot-items-smoke.md deleted file mode 100644 index c9b9c8a135..0000000000 --- a/.changeset/hot-items-smoke.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': minor ---- - -**BREAKING** Custom cluster suppliers need to cache their getClusters result - -To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters -the `getClusters` method is now called whenever the list of clusters is needed. - -Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters` -can be called frequently and should return a cached result from `getClusters` instead. - -For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`: - -```diff --import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; -+import { -+ ClusterDetails, -+ KubernetesBuilder, -+ KubernetesClustersSupplier, -+} from '@backstage/plugin-kubernetes-backend'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; -+import { Duration } from 'luxon'; -+ -+export class CustomClustersSupplier implements KubernetesClustersSupplier { -+ constructor(private clusterDetails: ClusterDetails[] = []) {} -+ -+ static create(refreshInterval: Duration) { -+ const clusterSupplier = new CustomClustersSupplier(); -+ // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts -+ runPeriodically( -+ () => clusterSupplier.refreshClusters(), -+ refreshInterval.toMillis(), -+ ); -+ return clusterSupplier; -+ } -+ -+ async refreshClusters(): Promise { -+ this.clusterDetails = []; // fetch from somewhere -+ } -+ -+ async getClusters(): Promise { -+ return this.clusterDetails; -+ } -+} - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { -- const { router } = await KubernetesBuilder.createBuilder({ -+ const builder = await KubernetesBuilder.createBuilder({ - logger: env.logger, - config: env.config, -- }).build(); -+ }); -+ builder.setClusterSupplier( -+ CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), -+ ); -+ const { router } = await builder.build(); -``` diff --git a/.changeset/hot-peaches-give.md b/.changeset/hot-peaches-give.md new file mode 100644 index 0000000000..ac30e8d492 --- /dev/null +++ b/.changeset/hot-peaches-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add package global-agent to support behind a proxy for backstage-cli commands like versions:bump. diff --git a/.changeset/itchy-candles-type.md b/.changeset/itchy-candles-type.md deleted file mode 100644 index 3507ecd867..0000000000 --- a/.changeset/itchy-candles-type.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Introduced a new MyGroupsSidebarItem SidebarItem that links to one or more groups based on the logged in user's membership. - -To use MyGroupsSidebarItem you'll need to add it to your `Root.tsx` like this: - -```diff -// app/src/components/Root/Root.tsx -+ import { MyGroupsSidebarItem } from '@backstage/plugin-org'; -+ import GroupIcon from '@material-ui/icons/People'; - - - - //... - }> - {/* Global nav, not org-specific */} - //... - -+ - //... - - - -``` diff --git a/.changeset/khaki-cougars-lick.md b/.changeset/khaki-cougars-lick.md deleted file mode 100644 index 1723cd0636..0000000000 --- a/.changeset/khaki-cougars-lick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Changed catalog filter components to only pay attention to query parameters relevant to the component. diff --git a/.changeset/khaki-pears-march.md b/.changeset/khaki-pears-march.md deleted file mode 100644 index 5626bf3ea5..0000000000 --- a/.changeset/khaki-pears-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': minor ---- - -**BREAKING**: All sign-in resolvers must now return a `token` in their sign-in result. Returning an `id` is no longer supported. diff --git a/.changeset/large-coins-arrive.md b/.changeset/large-coins-arrive.md deleted file mode 100644 index 3c0b3b9f97..0000000000 --- a/.changeset/large-coins-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Adjust the error messages when entities fail validation, to clearly state what entity that failed it diff --git a/.changeset/late-turtles-sparkle.md b/.changeset/late-turtles-sparkle.md deleted file mode 100644 index f0b28cdb87..0000000000 --- a/.changeset/late-turtles-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Declare oauth2 `clientSecret` with visibility secret diff --git a/.changeset/lazy-zebras-pay.md b/.changeset/lazy-zebras-pay.md new file mode 100644 index 0000000000..efcb62d7a9 --- /dev/null +++ b/.changeset/lazy-zebras-pay.md @@ -0,0 +1,39 @@ +--- +'@backstage/create-app': patch +--- + +Added some instruction comments to the generated config files, to clarify the +usage of `backend.baseUrl` and `backend.listen.host`. Importantly, it also per +default now listens on all IPv4 interfaces, to make it easier to take the step +over to production. If you want to do the same, update your +`app-config.production.yaml` as follows: + +```diff + backend: + listen: + port: 7007 ++ host: 0.0.0.0 +``` + +Also, updated the builtin backend Dockerfile to honor the +`app-config.production.yaml` file. If you want to do the same, change +`packages/backend/Dockerfile` as follows: + +```diff +-COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./ ++COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ + RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +-CMD ["node", "packages/backend", "--config", "app-config.yaml"] ++CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] +``` + +If you look carefully, this adds a glob match on app-config files. For those +that try out the build flows locally, you also want to make sure that the docker +daemon does NOT pick up any local/private config files that might contain +secrets. You should therefore also update your local `.dockerignore` file at the +same time: + +```diff ++*.local.yaml +``` diff --git a/.changeset/lemon-ears-build.md b/.changeset/lemon-ears-build.md deleted file mode 100644 index 87a04d150b..0000000000 --- a/.changeset/lemon-ears-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -Corrected title and URL to setup documentation in README diff --git a/.changeset/light-dragons-crash.md b/.changeset/light-dragons-crash.md deleted file mode 100644 index 35dea73ae6..0000000000 --- a/.changeset/light-dragons-crash.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Removed `@octokit/rest` and `@gitbeaker/node` from backend dependencies as these are unused in the default app. - -To apply these changes to your existing app, remove the following lines from the `dependencies` section of `packages/backend/package.json` - -```diff - "@backstage/plugin-techdocs-backend": "^1.0.0", -- "@gitbeaker/node": "^34.6.0", -- "@octokit/rest": "^18.5.3", -``` diff --git a/.changeset/light-drinks-rule.md b/.changeset/light-drinks-rule.md deleted file mode 100644 index 2b99f5988c..0000000000 --- a/.changeset/light-drinks-rule.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-search-backend': patch -'@backstage/plugin-search-backend-node': patch -'@backstage/plugin-search-backend-module-elasticsearch': patch -'@backstage/plugin-search-backend-module-pg': patch ---- - -Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. diff --git a/.changeset/little-moles-pull.md b/.changeset/little-moles-pull.md deleted file mode 100644 index f52bc24bb2..0000000000 --- a/.changeset/little-moles-pull.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Add type resolutions for `@types/react` and `types/react-dom`. - -The reason for this is the usage of `"@types/react": "*"` as a dependency which is very common practice in react packages. This recently resolves to react 18 which introduces several breaking changes in both internal and external packages. - -To apply these changes to your existing installation, add a resolutions block to your `package.json` - -```json - "resolutions": { - "@types/react": "^17", - "@types/react-dom": "^17" - }, -``` - -If your existing app depends on react 16, use this resolution block instead. - -```json - "resolutions": { - "@types/react": "^16", - "@types/react-dom": "^16" - }, -``` diff --git a/.changeset/many-cameras-search.md b/.changeset/many-cameras-search.md deleted file mode 100644 index 6998dfd713..0000000000 --- a/.changeset/many-cameras-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Added `spec.profile.displayName` to search index for Group kinds diff --git a/.changeset/mean-flowers-change.md b/.changeset/mean-flowers-change.md new file mode 100644 index 0000000000..7634a20c84 --- /dev/null +++ b/.changeset/mean-flowers-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Override default commit message and author details in GitHub, Azure, bitbucket diff --git a/.changeset/mean-pumas-search.md b/.changeset/mean-pumas-search.md deleted file mode 100644 index 68a3aebc85..0000000000 --- a/.changeset/mean-pumas-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix diff --git a/.changeset/mean-rabbits-tell.md b/.changeset/mean-rabbits-tell.md deleted file mode 100644 index 88d853d9b3..0000000000 --- a/.changeset/mean-rabbits-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-stack-overflow-backend': minor ---- - -Add stack overflow backend plugin diff --git a/.changeset/mean-tomatoes-shout.md b/.changeset/mean-tomatoes-shout.md deleted file mode 100644 index 446e66d386..0000000000 --- a/.changeset/mean-tomatoes-shout.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-permission-backend': patch -'@backstage/plugin-permission-react': patch -'@backstage/plugin-search-backend': patch -'@backstage/test-utils': patch ---- - -Use updated types from `@backstage/plugin-permission-common` diff --git a/.changeset/metal-hairs-build.md b/.changeset/metal-hairs-build.md new file mode 100644 index 0000000000..9ed8bba01b --- /dev/null +++ b/.changeset/metal-hairs-build.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-rollbar-backend': patch +--- + +Updated README to include clearer installation instructions on how to install and configure. diff --git a/.changeset/metal-pants-fly.md b/.changeset/metal-pants-fly.md new file mode 100644 index 0000000000..7db2014377 --- /dev/null +++ b/.changeset/metal-pants-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +star icons now have the same yellow color as the other star icons when a entity is favourite diff --git a/.changeset/metal-queens-cheat.md b/.changeset/metal-queens-cheat.md deleted file mode 100644 index b2d152b3a9..0000000000 --- a/.changeset/metal-queens-cheat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-common': patch -'@backstage/plugin-jenkins-common': patch ---- - -Use `createPermission` helper when creating permissions. diff --git a/.changeset/mighty-suns-drop.md b/.changeset/mighty-suns-drop.md deleted file mode 100644 index f8bb686aaf..0000000000 --- a/.changeset/mighty-suns-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -add aggregated ownership type for kind group in OwnershipCard diff --git a/.changeset/modern-actors-notice.md b/.changeset/modern-actors-notice.md deleted file mode 100644 index 20dde7f42d..0000000000 --- a/.changeset/modern-actors-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**BREAKING:** Mark CatalogBuilder#addPermissionRules as @alpha. diff --git a/.changeset/modern-pumas-join.md b/.changeset/modern-pumas-join.md deleted file mode 100644 index d2995af986..0000000000 --- a/.changeset/modern-pumas-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Enable internal batching of very large deletions, to not run into SQL binding limits diff --git a/.changeset/moody-apes-tickle.md b/.changeset/moody-apes-tickle.md deleted file mode 100644 index bdf593c9ad..0000000000 --- a/.changeset/moody-apes-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Added tableOptions property to CatalogTable and DefaultCatalogPage to support customization of the Catalog Table. Related issue #10453 diff --git a/.changeset/moody-ducks-return.md b/.changeset/moody-ducks-return.md deleted file mode 100644 index 090b2f5db4..0000000000 --- a/.changeset/moody-ducks-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Add new `draft` option to the `publish:github:pull-request` action. diff --git a/.changeset/moody-laws-boil.md b/.changeset/moody-laws-boil.md new file mode 100644 index 0000000000..f481e84044 --- /dev/null +++ b/.changeset/moody-laws-boil.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': minor +'@backstage/plugin-catalog-react': minor +--- + +Use InfoCardVariants on custom cards variant attribute diff --git a/.changeset/moody-pigs-rush.md b/.changeset/moody-pigs-rush.md deleted file mode 100644 index 37ae171382..0000000000 --- a/.changeset/moody-pigs-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Applied the fix from version 0.12.3 of this package, which is part of the v1.0.1 release of Backstage. diff --git a/.changeset/moody-sheep-marry.md b/.changeset/moody-sheep-marry.md deleted file mode 100644 index 73f037db32..0000000000 --- a/.changeset/moody-sheep-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -The Template Preview page has been renamed to Template Editor, and is not available at the `/edit` path instead. There is a redirect in place from `/preview`. diff --git a/.changeset/moody-suns-smell.md b/.changeset/moody-suns-smell.md new file mode 100644 index 0000000000..b051335ad7 --- /dev/null +++ b/.changeset/moody-suns-smell.md @@ -0,0 +1,37 @@ +--- +'@backstage/integration': minor +'@backstage/integration-react': minor +--- + +Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer` +(backwards compatible). + +In order to migrate to the new integration configs, +move your configs from `integrations.bitbucket` +to `integrations.bitbucketCloud` or `integrations.bitbucketServer`. + +Migration example: + +**Before:** + +```yaml +integrations: + bitbucket: + - host: bitbucket.org + username: bitbucket_user + appPassword: app-password + - host: bitbucket-server.company.com + token: my-token +``` + +**After:** + +```yaml +integrations: + bitbucketCloud: + - username: bitbucket_user + appPassword: app-password + bitbucketServer: + - host: bitbucket-server.company.com + token: my-token +``` diff --git a/.changeset/nasty-humans-give.md b/.changeset/nasty-humans-give.md new file mode 100644 index 0000000000..85ce3149ba --- /dev/null +++ b/.changeset/nasty-humans-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Added the prop `NotFoundComponent` to `EntityLayout` which can be used to include a custom component when an entity is not found in the catalog diff --git a/.changeset/new-beds-argue.md b/.changeset/new-beds-argue.md new file mode 100644 index 0000000000..8aa8acf09d --- /dev/null +++ b/.changeset/new-beds-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Bumped @asyncapi/react-component > openapi-sampler > json-pointer to resolve security vulnerability CVE-2021-23820 diff --git a/.changeset/new-lions-run.md b/.changeset/new-lions-run.md deleted file mode 100644 index 8e830b33fa..0000000000 --- a/.changeset/new-lions-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-graph': patch ---- - -Added renderNode and renderLabel property to EntityRelationsGraph to support customization using CustomNode and CustomLabel components diff --git a/.changeset/new-singers-lie.md b/.changeset/new-singers-lie.md deleted file mode 100644 index d7101db4d0..0000000000 --- a/.changeset/new-singers-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend': patch ---- - -Updated README.md with better install instructions diff --git a/.changeset/nine-geese-rest.md b/.changeset/nine-geese-rest.md new file mode 100644 index 0000000000..da91c6234b --- /dev/null +++ b/.changeset/nine-geese-rest.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': patch +--- + +Changed Rollup configuration for TypeScript definition plugin to ignore `css`, +`scss`, `sass`, `svg`, `eot`, `woff`, `woff2` and `ttf` files. diff --git a/.changeset/nine-grapes-turn.md b/.changeset/nine-grapes-turn.md deleted file mode 100644 index 263618722c..0000000000 --- a/.changeset/nine-grapes-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Added the GerritUrlReader that implements "readUrl". diff --git a/.changeset/ninety-eggs-argue.md b/.changeset/ninety-eggs-argue.md deleted file mode 100644 index 6fd221b722..0000000000 --- a/.changeset/ninety-eggs-argue.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': minor -'@backstage/create-app': patch ---- - -**BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. - -`Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. - -NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. - -To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: - -```diff -+import { Duration } from 'luxon'; - -/* ... */ - -+ const schedule = env.scheduler.createScheduledTaskRunner({ -+ frequency: Duration.fromObject({ minutes: 10 }), -+ timeout: Duration.fromObject({ minutes: 15 }), -+ initialDelay: Duration.fromObject({ seconds: 3 }), -+ }); - - indexBuilder.addCollator({ -- defaultRefreshIntervalSeconds: 600, -+ schedule, - factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { - discovery: env.discovery, - tokenManager: env.tokenManager, - }), - }); - - indexBuilder.addCollator({ -- defaultRefreshIntervalSeconds: 600, -+ schedule, - factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { - discovery: env.discovery, - tokenManager: env.tokenManager, - }), - }); - - const { scheduler } = await indexBuilder.build(); -- setTimeout(() => scheduler.start(), 3000); -+ scheduler.start(); -/* ... */ -``` - -NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure -the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): - -```diff -+import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; - -/* ... */ - -+ const schedule: TaskRunner = { -+ run: async (task: TaskInvocationDefinition) => { -+ const startRefresh = async () => { -+ while (!task.signal?.aborted) { -+ try { -+ await task.fn(task.signal); -+ } catch { -+ // ignore intentionally -+ } -+ -+ await new Promise(resolve => setTimeout(resolve, 600 * 1000)); -+ } -+ }; -+ startRefresh(); -+ }, -+ }; - - indexBuilder.addCollator({ -- defaultRefreshIntervalSeconds: 600, -+ schedule, - factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { - discovery: env.discovery, - tokenManager: env.tokenManager, - }), - }); - -/* ... */ -``` diff --git a/.changeset/ninety-fishes-vanish.md b/.changeset/ninety-fishes-vanish.md deleted file mode 100644 index b0d3ac370f..0000000000 --- a/.changeset/ninety-fishes-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Switch to `SearchDocument` type in `DefaultResultListItem` props diff --git a/.changeset/ninety-islands-report.md b/.changeset/ninety-islands-report.md deleted file mode 100644 index 4a515b75cd..0000000000 --- a/.changeset/ninety-islands-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Defensively encode URL parameters when fetching ELB keys diff --git a/.changeset/ninety-radios-protect.md b/.changeset/ninety-radios-protect.md deleted file mode 100644 index fa3fae281f..0000000000 --- a/.changeset/ninety-radios-protect.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/backend-tasks': patch ---- - -Fixed the `initialDelay` parameter of tasks to properly make task workers -_always_ wait before the first invocations on startup, not just the very first -time that the task is ever created. This behavior is more in line with -expectations. Callers to not need to update their code. - -Also clarified in the doc comment for the field that this wait applies only on -an individual worker level. That is, if you have a cluster of workers then each -individual machine may postpone its first task invocation by the given amount of -time to leave room for the service to settle, but _other_ workers may still -continue to invoke the task on the regular cadence in the meantime. diff --git a/.changeset/old-bikes-study.md b/.changeset/old-bikes-study.md new file mode 100644 index 0000000000..f2452fcce3 --- /dev/null +++ b/.changeset/old-bikes-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Gerrit Integration: Added the `getGerritProjectsApiUrl` function diff --git a/.changeset/olive-geese-chew.md b/.changeset/olive-geese-chew.md deleted file mode 100644 index a08b73b4a7..0000000000 --- a/.changeset/olive-geese-chew.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/integration': patch ---- - -Support external ID when assuming roles in S3 integration - -In order to assume a role created by a 3rd party as external -ID is needed. This change adds an optional field to the s3 -integration configuration and consumes that in the AwsS3UrlReader. diff --git a/.changeset/perfect-penguins-rescue.md b/.changeset/perfect-penguins-rescue.md new file mode 100644 index 0000000000..ab55355121 --- /dev/null +++ b/.changeset/perfect-penguins-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gerrit': minor +--- + +Initial version of the `plugin-catalog-backend-module-gerrit` plugin diff --git a/.changeset/perfect-phones-roll.md b/.changeset/perfect-phones-roll.md deleted file mode 100644 index c1f9ecdbc1..0000000000 --- a/.changeset/perfect-phones-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Add helpful README.md files in the original `packages` and `plugins` folders diff --git a/.changeset/pink-mayflies-rhyme.md b/.changeset/pink-mayflies-rhyme.md new file mode 100644 index 0000000000..fff17b28b3 --- /dev/null +++ b/.changeset/pink-mayflies-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic-dashboard': patch +--- + +Add `FetchApi` and related `fetchApiRef` which implement fetch. in order to included the Authorization Token to New Relic Dashboard Plugin, so that frontend can send an authenticated proxy call to backend diff --git a/.changeset/polite-planets-learn.md b/.changeset/polite-planets-learn.md new file mode 100644 index 0000000000..283af6e801 --- /dev/null +++ b/.changeset/polite-planets-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Fix linking ownership card to catalog owner filter when namespaces are used diff --git a/.changeset/poor-knives-boil.md b/.changeset/poor-knives-boil.md deleted file mode 100644 index dfdc09cdbe..0000000000 --- a/.changeset/poor-knives-boil.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-react': patch ---- - -Columns in CatalogTable now change depending on the entity kind, ensuring only relevant columns are displayed. diff --git a/.changeset/poor-pumpkins-fly.md b/.changeset/poor-pumpkins-fly.md deleted file mode 100644 index d4462d0010..0000000000 --- a/.changeset/poor-pumpkins-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-azure-devops-backend': patch ---- - -Stop loading all teams when plugin is initialized diff --git a/.changeset/popular-geese-prove.md b/.changeset/popular-geese-prove.md deleted file mode 100644 index 1df79d06c7..0000000000 --- a/.changeset/popular-geese-prove.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated the plugin template to install version 14 of `@testing-library/user-event`. - -To apply this change to your own project, update the `devDependencies` section in your `package.json` files: - -```diff - "devDependencies": { - ... omitted dev dependencies ... -- "@testing-library/user-event": "^13.1.8", -+ "@testing-library/user-event": "^14.0.0", - ... omitted dev dependencies ... - } -``` diff --git a/.changeset/pre.json b/.changeset/pre.json index 27d3eeea2f..f598c9f725 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,258 +2,195 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.69", - "@backstage/app-defaults": "1.0.0", - "example-backend": "0.2.69", - "@backstage/backend-common": "0.13.1", - "@backstage/backend-tasks": "0.2.1", - "@backstage/backend-test-utils": "0.1.22", - "@backstage/catalog-client": "1.0.0", - "@backstage/catalog-model": "1.0.0", - "@backstage/cli": "0.16.0", + "example-app": "0.2.70", + "@backstage/app-defaults": "1.0.1", + "example-backend": "0.2.70", + "@backstage/backend-common": "0.13.2", + "@backstage/backend-tasks": "0.3.0", + "@backstage/backend-test-utils": "0.1.23", + "@backstage/catalog-client": "1.0.1", + "@backstage/catalog-model": "1.0.1", + "@backstage/cli": "0.17.0", "@backstage/cli-common": "0.1.8", - "@backstage/codemods": "0.1.36", + "@backstage/codemods": "0.1.37", "@backstage/config": "1.0.0", - "@backstage/config-loader": "1.0.0", - "@backstage/core-app-api": "1.0.0", - "@backstage/core-components": "0.9.2", - "@backstage/core-plugin-api": "1.0.0", - "@backstage/create-app": "0.4.24", - "@backstage/dev-utils": "1.0.0", + "@backstage/config-loader": "1.1.0", + "@backstage/core-app-api": "1.0.1", + "@backstage/core-components": "0.9.3", + "@backstage/core-plugin-api": "1.0.1", + "@backstage/create-app": "0.4.26", + "@backstage/dev-utils": "1.0.1", "e2e-test": "0.2.0", "@backstage/errors": "1.0.0", - "@backstage/integration": "1.0.0", - "@backstage/integration-react": "1.0.0", + "@backstage/integration": "1.1.0", + "@backstage/integration-react": "1.0.1", "@backstage/release-manifests": "0.0.2", - "@backstage/search-common": "0.3.2", - "@techdocs/cli": "1.0.0", - "techdocs-cli-embedded-app": "0.2.68", - "@backstage/techdocs-common": "0.11.13", - "@backstage/test-utils": "1.0.0", + "@backstage/search-common": "0.3.3", + "@techdocs/cli": "1.1.0", + "techdocs-cli-embedded-app": "0.2.69", + "@backstage/techdocs-common": "0.11.14", + "@backstage/test-utils": "1.0.1", "@backstage/theme": "0.2.15", "@backstage/types": "1.0.0", - "@backstage/version-bridge": "1.0.0", - "@backstage/plugin-airbrake": "0.3.3", - "@backstage/plugin-airbrake-backend": "0.2.3", - "@backstage/plugin-allure": "0.1.19", - "@backstage/plugin-analytics-module-ga": "0.1.14", - "@backstage/plugin-apache-airflow": "0.1.11", - "@backstage/plugin-api-docs": "0.8.3", - "@backstage/plugin-app-backend": "0.3.30", - "@backstage/plugin-auth-backend": "0.12.2", - "@backstage/plugin-auth-node": "0.1.6", - "@backstage/plugin-azure-devops": "0.1.19", - "@backstage/plugin-azure-devops-backend": "0.3.9", + "@backstage/version-bridge": "1.0.1", + "@backstage/plugin-airbrake": "0.3.4", + "@backstage/plugin-airbrake-backend": "0.2.4", + "@backstage/plugin-allure": "0.1.20", + "@backstage/plugin-analytics-module-ga": "0.1.15", + "@backstage/plugin-apache-airflow": "0.1.12", + "@backstage/plugin-api-docs": "0.8.4", + "@backstage/plugin-app-backend": "0.3.31", + "@backstage/plugin-auth-backend": "0.13.0", + "@backstage/plugin-auth-node": "0.2.0", + "@backstage/plugin-azure-devops": "0.1.20", + "@backstage/plugin-azure-devops-backend": "0.3.10", "@backstage/plugin-azure-devops-common": "0.2.2", - "@backstage/plugin-badges": "0.2.27", - "@backstage/plugin-badges-backend": "0.1.24", - "@backstage/plugin-bazaar": "0.1.18", - "@backstage/plugin-bazaar-backend": "0.1.14", - "@backstage/plugin-bitrise": "0.1.30", - "@backstage/plugin-catalog": "1.0.0", - "@backstage/plugin-catalog-backend": "1.0.0", - "@backstage/plugin-catalog-backend-module-aws": "0.1.3", - "@backstage/plugin-catalog-backend-module-azure": "0.1.1", - "@backstage/plugin-catalog-backend-module-bitbucket": "0.1.1", - "@backstage/plugin-catalog-backend-module-github": "0.1.1", - "@backstage/plugin-catalog-backend-module-gitlab": "0.1.1", - "@backstage/plugin-catalog-backend-module-ldap": "0.4.1", - "@backstage/plugin-catalog-backend-module-msgraph": "0.3.0", - "@backstage/plugin-catalog-common": "1.0.0", - "@backstage/plugin-catalog-graph": "0.2.15", - "@backstage/plugin-catalog-graphql": "0.3.7", - "@backstage/plugin-catalog-import": "0.8.6", - "@backstage/plugin-catalog-react": "1.0.0", - "@backstage/plugin-cicd-statistics": "0.1.5", - "@backstage/plugin-circleci": "0.3.3", - "@backstage/plugin-cloudbuild": "0.3.3", - "@backstage/plugin-code-climate": "0.1.3", - "@backstage/plugin-code-coverage": "0.1.30", - "@backstage/plugin-code-coverage-backend": "0.1.28", - "@backstage/plugin-config-schema": "0.1.26", - "@backstage/plugin-cost-insights": "0.11.25", - "@backstage/plugin-explore": "0.3.34", - "@backstage/plugin-explore-react": "0.0.15", - "@backstage/plugin-firehydrant": "0.1.20", - "@backstage/plugin-fossa": "0.2.35", - "@backstage/plugin-gcalendar": "0.2.0", - "@backstage/plugin-gcp-projects": "0.3.22", - "@backstage/plugin-git-release-manager": "0.3.16", - "@backstage/plugin-github-actions": "0.5.3", - "@backstage/plugin-github-deployments": "0.1.34", - "@backstage/plugin-gitops-profiles": "0.3.21", - "@backstage/plugin-gocd": "0.1.9", - "@backstage/plugin-graphiql": "0.2.35", - "@backstage/plugin-graphql-backend": "0.1.20", - "@backstage/plugin-home": "0.4.19", - "@backstage/plugin-ilert": "0.1.29", - "@backstage/plugin-jenkins": "0.7.2", - "@backstage/plugin-jenkins-backend": "0.1.19", - "@backstage/plugin-jenkins-common": "0.1.2", - "@backstage/plugin-kafka": "0.3.3", - "@backstage/plugin-kafka-backend": "0.2.23", - "@backstage/plugin-kubernetes": "0.6.3", - "@backstage/plugin-kubernetes-backend": "0.4.13", - "@backstage/plugin-kubernetes-common": "0.2.8", - "@backstage/plugin-lighthouse": "0.3.3", - "@backstage/plugin-newrelic": "0.3.21", - "@backstage/plugin-newrelic-dashboard": "0.1.11", - "@backstage/plugin-org": "0.5.3", - "@backstage/plugin-pagerduty": "0.3.30", - "@backstage/plugin-periskop": "0.1.1", - "@backstage/plugin-periskop-backend": "0.1.1", - "@backstage/plugin-permission-backend": "0.5.5", - "@backstage/plugin-permission-common": "0.5.3", - "@backstage/plugin-permission-node": "0.5.5", - "@backstage/plugin-permission-react": "0.3.4", - "@backstage/plugin-proxy-backend": "0.2.24", - "@backstage/plugin-rollbar": "0.4.3", - "@backstage/plugin-rollbar-backend": "0.1.27", - "@backstage/plugin-scaffolder": "1.0.0", - "@backstage/plugin-scaffolder-backend": "1.0.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.5", - "@backstage/plugin-scaffolder-backend-module-rails": "0.3.5", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.3", - "@backstage/plugin-scaffolder-common": "1.0.0", - "@backstage/plugin-search": "0.7.4", - "@backstage/plugin-search-backend": "0.4.8", - "@backstage/plugin-search-backend-module-elasticsearch": "0.1.2", - "@backstage/plugin-search-backend-module-pg": "0.3.1", - "@backstage/plugin-search-backend-node": "0.5.2", - "@backstage/plugin-search-common": "0.3.2", - "@backstage/plugin-sentry": "0.3.41", - "@backstage/plugin-shortcuts": "0.2.4", - "@backstage/plugin-sonarqube": "0.3.3", - "@backstage/plugin-splunk-on-call": "0.3.27", - "@backstage/plugin-tech-insights": "0.1.13", - "@backstage/plugin-tech-insights-backend": "0.2.10", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.14", + "@backstage/plugin-badges": "0.2.28", + "@backstage/plugin-badges-backend": "0.1.25", + "@backstage/plugin-bazaar": "0.1.19", + "@backstage/plugin-bazaar-backend": "0.1.15", + "@backstage/plugin-bitrise": "0.1.31", + "@backstage/plugin-catalog": "1.1.0", + "@backstage/plugin-catalog-backend": "1.1.0", + "@backstage/plugin-catalog-backend-module-aws": "0.1.4", + "@backstage/plugin-catalog-backend-module-azure": "0.1.2", + "@backstage/plugin-catalog-backend-module-bitbucket": "0.1.2", + "@backstage/plugin-catalog-backend-module-github": "0.1.2", + "@backstage/plugin-catalog-backend-module-gitlab": "0.1.2", + "@backstage/plugin-catalog-backend-module-ldap": "0.4.2", + "@backstage/plugin-catalog-backend-module-msgraph": "0.3.1", + "@backstage/plugin-catalog-common": "1.0.1", + "@backstage/plugin-catalog-graph": "0.2.16", + "@backstage/plugin-catalog-graphql": "0.3.8", + "@backstage/plugin-catalog-import": "0.8.7", + "@backstage/plugin-catalog-react": "1.0.1", + "@backstage/plugin-cicd-statistics": "0.1.6", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.0", + "@backstage/plugin-circleci": "0.3.4", + "@backstage/plugin-cloudbuild": "0.3.4", + "@backstage/plugin-code-climate": "0.1.4", + "@backstage/plugin-code-coverage": "0.1.31", + "@backstage/plugin-code-coverage-backend": "0.1.29", + "@backstage/plugin-config-schema": "0.1.27", + "@backstage/plugin-cost-insights": "0.11.26", + "@backstage/plugin-explore": "0.3.35", + "@backstage/plugin-explore-react": "0.0.16", + "@backstage/plugin-firehydrant": "0.1.21", + "@backstage/plugin-fossa": "0.2.36", + "@backstage/plugin-gcalendar": "0.3.0", + "@backstage/plugin-gcp-projects": "0.3.23", + "@backstage/plugin-git-release-manager": "0.3.17", + "@backstage/plugin-github-actions": "0.5.4", + "@backstage/plugin-github-deployments": "0.1.35", + "@backstage/plugin-gitops-profiles": "0.3.22", + "@backstage/plugin-gocd": "0.1.10", + "@backstage/plugin-graphiql": "0.2.36", + "@backstage/plugin-graphql-backend": "0.1.21", + "@backstage/plugin-home": "0.4.20", + "@backstage/plugin-ilert": "0.1.30", + "@backstage/plugin-jenkins": "0.7.3", + "@backstage/plugin-jenkins-backend": "0.1.20", + "@backstage/plugin-jenkins-common": "0.1.3", + "@backstage/plugin-kafka": "0.3.4", + "@backstage/plugin-kafka-backend": "0.2.24", + "@backstage/plugin-kubernetes": "0.6.4", + "@backstage/plugin-kubernetes-backend": "0.5.0", + "@backstage/plugin-kubernetes-common": "0.2.9", + "@backstage/plugin-lighthouse": "0.3.4", + "@backstage/plugin-newrelic": "0.3.22", + "@backstage/plugin-newrelic-dashboard": "0.1.12", + "@backstage/plugin-org": "0.5.4", + "@backstage/plugin-pagerduty": "0.3.31", + "@backstage/plugin-periskop": "0.1.2", + "@backstage/plugin-periskop-backend": "0.1.2", + "@backstage/plugin-permission-backend": "0.5.6", + "@backstage/plugin-permission-common": "0.6.0", + "@backstage/plugin-permission-node": "0.6.0", + "@backstage/plugin-permission-react": "0.4.0", + "@backstage/plugin-proxy-backend": "0.2.25", + "@backstage/plugin-rollbar": "0.4.4", + "@backstage/plugin-rollbar-backend": "0.1.28", + "@backstage/plugin-scaffolder": "1.1.0", + "@backstage/plugin-scaffolder-backend": "1.1.0", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.6", + "@backstage/plugin-scaffolder-backend-module-rails": "0.3.6", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.4", + "@backstage/plugin-scaffolder-common": "1.0.1", + "@backstage/plugin-search": "0.8.0", + "@backstage/plugin-search-backend": "0.5.0", + "@backstage/plugin-search-backend-module-elasticsearch": "0.1.3", + "@backstage/plugin-search-backend-module-pg": "0.3.2", + "@backstage/plugin-search-backend-node": "0.6.0", + "@backstage/plugin-search-common": "0.3.3", + "@backstage/plugin-search-react": "0.1.0", + "@backstage/plugin-sentry": "0.3.42", + "@backstage/plugin-shortcuts": "0.2.5", + "@backstage/plugin-sonarqube": "0.3.4", + "@backstage/plugin-splunk-on-call": "0.3.28", + "@backstage/plugin-stack-overflow": "0.1.0", + "@backstage/plugin-stack-overflow-backend": "0.1.0", + "@backstage/plugin-tech-insights": "0.2.0", + "@backstage/plugin-tech-insights-backend": "0.3.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.15", "@backstage/plugin-tech-insights-common": "0.2.4", - "@backstage/plugin-tech-insights-node": "0.2.8", - "@backstage/plugin-tech-radar": "0.5.10", - "@backstage/plugin-techdocs": "1.0.0", - "@backstage/plugin-techdocs-backend": "1.0.0", - "@backstage/plugin-techdocs-node": "1.0.0", - "@backstage/plugin-todo": "0.2.5", - "@backstage/plugin-todo-backend": "0.1.27", - "@backstage/plugin-user-settings": "0.4.2", - "@backstage/plugin-xcmetrics": "0.2.23", - "@backstage/plugin-stack-overflow": "0.0.0", - "@backstage/plugin-stack-overflow-backend": "0.0.0" + "@backstage/plugin-tech-insights-node": "0.2.9", + "@backstage/plugin-tech-radar": "0.5.11", + "@backstage/plugin-techdocs": "1.1.0", + "@backstage/plugin-techdocs-backend": "1.1.0", + "@backstage/plugin-techdocs-node": "1.1.0", + "@backstage/plugin-techdocs-react": "0.1.0", + "@backstage/plugin-todo": "0.2.6", + "@backstage/plugin-todo-backend": "0.1.28", + "@backstage/plugin-user-settings": "0.4.3", + "@backstage/plugin-xcmetrics": "0.2.24", + "@backstage/plugin-catalog-backend-module-gerrit": "0.0.0", + "@backstage/plugin-techdocs-module-addons-contrib": "0.0.0" }, "changesets": [ - "afraid-boats-check", - "angry-pens-begin", - "beige-avocados-matter", - "beige-baboons-walk", - "big-buses-clap", - "big-mayflies-sin", - "blue-beers-kiss", - "brave-chairs-stare", - "breezy-schools-compete", - "calm-walls-heal", - "clean-lions-taste", - "clever-donuts-teach", - "dependabot-05b19b9", - "dependabot-2b68456", - "dependabot-51da438", - "dependabot-6754062", - "dependabot-7272684", - "dependabot-84caed8", - "dependabot-c1fe92f", - "dependabot-c762551", - "dependabot-ebb092c", - "dry-fans-arrive", - "eleven-days-brush", - "eleven-frogs-promise", - "eleven-pens-collect", - "empty-pens-invent", - "fair-dingos-glow", - "fair-lamps-leave", - "famous-seas-flash", - "fast-cheetahs-grow", - "few-buttons-breathe", - "few-mayflies-divide", - "forty-poets-tie", - "four-birds-peel", - "fresh-bulldogs-own", - "fresh-cobras-admire", - "fresh-dodos-rush", - "grumpy-parents-prove", - "happy-foxes-arrive", - "happy-mugs-camp", - "hip-poems-breathe", - "itchy-candles-type", - "khaki-cougars-lick", - "khaki-pears-march", - "large-coins-arrive", - "light-drinks-rule", - "mean-pumas-search", - "mean-rabbits-tell", - "mean-tomatoes-shout", - "metal-queens-cheat", - "mighty-suns-drop", - "modern-actors-notice", - "modern-pumas-join", - "moody-apes-tickle", - "moody-ducks-return", - "moody-pigs-rush", - "new-lions-run", - "new-singers-lie", - "nine-grapes-turn", - "ninety-fishes-vanish", - "ninety-radios-protect", - "olive-geese-chew", - "perfect-phones-roll", - "poor-knives-boil", - "poor-pumpkins-fly", - "pretty-ghosts-scream", - "purple-boats-punch", - "purple-laws-give", - "rare-waves-hammer", - "red-snakes-float", - "rich-maps-hear", - "rotten-planes-watch", - "rude-toes-compare", - "seven-apricots-sell", - "sharp-terms-tap", - "shiny-seas-yell", - "short-knives-wave", - "silent-bobcats-matter", - "silly-llamas-relax", - "silver-drinks-study", - "silver-panthers-occur", - "silver-pots-call", - "smart-phones-exist", - "sour-cameras-deliver", - "sour-lobsters-sniff", - "stale-carrots-smile", - "strange-wasps-whisper", - "stupid-ways-think", - "swift-fans-shout", - "swift-trees-rest", - "tame-cars-press", - "tasty-beans-clap", - "tasty-emus-hunt", - "tasty-goats-shout", - "tasty-pears-shake", - "techdocs-coats-obey", - "techdocs-head-shoulders-knees-toes", - "techdocs-parents-suffer", - "thick-comics-fold", - "thin-deers-turn", - "thin-lizards-battle", - "tidy-days-warn", - "tidy-emus-stare", - "twelve-birds-wait", - "twelve-buses-nail", - "unlucky-schools-heal", - "unlucky-snakes-turn", - "warm-actors-attend", - "warm-mangos-compete", - "wicked-beds-return", - "wicked-feet-clap", - "witty-years-sniff", - "yellow-hats-remember" + "breezy-days-prove", + "cool-ties-share", + "curly-parrots-applaud", + "dirty-eagles-hunt", + "eighty-chicken-retire", + "fair-kings-retire", + "fuzzy-seahorses-teach", + "giant-cheetahs-thank", + "hip-adults-collect", + "honest-kids-live", + "hot-peaches-give", + "lazy-zebras-pay", + "mean-flowers-change", + "metal-pants-fly", + "moody-laws-boil", + "moody-suns-smell", + "nasty-humans-give", + "new-beds-argue", + "old-bikes-study", + "perfect-penguins-rescue", + "pink-mayflies-rhyme", + "renovate-bf43c44", + "search-dry-wolves-join", + "search-heavy-llamas-worry", + "seven-deers-rule", + "shiny-students-approve", + "short-dodos-sparkle", + "short-flies-collect", + "silver-readers-deliver", + "sixty-llamas-change", + "spotty-plums-rule", + "stale-pigs-reply", + "strong-mangos-sell", + "swift-bugs-share", + "swift-parrots-hammer", + "tasty-drinks-teach", + "techdocs-changeset-not-found", + "techdocs-nice-boats-wonder", + "techdocs-quick-owls-smile", + "techdocs-touch-screen-wipes", + "thick-bees-brush", + "tough-forks-carry", + "violet-steaks-knock", + "wild-pigs-work", + "wise-emus-wait" ] } diff --git a/.changeset/pretty-ghosts-scream.md b/.changeset/pretty-ghosts-scream.md deleted file mode 100644 index 7a604d75b6..0000000000 --- a/.changeset/pretty-ghosts-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -`TestDatabases.create` will no longer set up an `afterAll` test handler if no databases are supported. diff --git a/.changeset/proud-news-perform.md b/.changeset/proud-news-perform.md deleted file mode 100644 index 9d5cbae8ab..0000000000 --- a/.changeset/proud-news-perform.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-cicd-statistics-module-gitlab': minor ---- - -Created a module to extract the CI/CD statistics from a Gitlab repository. -Read the `README.md` in the `cicd-statistics-module-gitlab` plugin folder on how to set it up. diff --git a/.changeset/proud-teachers-draw.md b/.changeset/proud-teachers-draw.md deleted file mode 100644 index dbce78c3c2..0000000000 --- a/.changeset/proud-teachers-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Updated the "unregister location" behavior in `UnregisterEntityDialog`. Removed unnecessary entity deletion requests that were sent after successfully deleting a location. diff --git a/.changeset/purple-boats-punch.md b/.changeset/purple-boats-punch.md deleted file mode 100644 index 817f0a79bc..0000000000 --- a/.changeset/purple-boats-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -**DEPRECATION**: The `tokenIssuer` option for `OAuthAdapter` is no longer needed and has been deprecated. diff --git a/.changeset/purple-laws-give.md b/.changeset/purple-laws-give.md deleted file mode 100644 index 7e985092e2..0000000000 --- a/.changeset/purple-laws-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Removed broken link from Labels section of entity inspector. diff --git a/.changeset/quick-avocados-sell.md b/.changeset/quick-avocados-sell.md deleted file mode 100644 index 74014dca66..0000000000 --- a/.changeset/quick-avocados-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds. diff --git a/.changeset/quiet-ties-happen.md b/.changeset/quiet-ties-happen.md deleted file mode 100644 index ece2ff7e23..0000000000 --- a/.changeset/quiet-ties-happen.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -'@backstage/app-defaults': patch -'@backstage/cli': patch -'@backstage/codemods': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/core-components': patch -'@backstage/core-plugin-api': patch -'@backstage/create-app': patch -'@backstage/integration-react': patch -'@techdocs/cli': patch -'@backstage/test-utils': patch -'@backstage/plugin-airbrake': patch -'@backstage/plugin-allure': patch -'@backstage/plugin-analytics-module-ga': patch -'@backstage/plugin-apache-airflow': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-azure-devops': patch -'@backstage/plugin-badges': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-code-climate': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-explore-react': patch -'@backstage/plugin-firehydrant': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-gcalendar': patch -'@backstage/plugin-gcp-projects': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-gocd': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-home': patch -'@backstage/plugin-ilert': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-newrelic': patch -'@backstage/plugin-org': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-periskop': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-search': patch -'@backstage/plugin-search-backend-node': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-shortcuts': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-stack-overflow': patch -'@backstage/plugin-tech-insights': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-todo': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-xcmetrics': patch ---- - -Bump to using `@types/node` v16 diff --git a/.changeset/rare-emus-agree.md b/.changeset/rare-emus-agree.md deleted file mode 100644 index 423d4c6259..0000000000 --- a/.changeset/rare-emus-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -feature: provide access token to JenkinsInstanceConfig. It can be passed to other backend calls if authentication enabled. DefaultJenkinsInfoProvider sends always this token to catalog api if access token exists. diff --git a/.changeset/rare-parents-pretend.md b/.changeset/rare-parents-pretend.md deleted file mode 100644 index f211a85efe..0000000000 --- a/.changeset/rare-parents-pretend.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Accept `PermissionEvaluator` instead of the deprecated `PermissionAuthorizer`. - -Apply the following to `packages/backend/src/types.ts`: - -```diff -- import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -+ import { PermissionEvaluator } from '@backstage/plugin-permission-common'; - - export type PluginEnvironment = { - ... - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - scheduler: PluginTaskScheduler; -- permissions: PermissionAuthorizer; -+ permissions: PermissionEvaluator; - }; -``` diff --git a/.changeset/rare-waves-hammer.md b/.changeset/rare-waves-hammer.md deleted file mode 100644 index 5b9f7c845d..0000000000 --- a/.changeset/rare-waves-hammer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-common': patch ---- - -Add `@alpha` `CatalogEntityPermission` convenience type, available for import from `@backstage/plugin-catalog-common/alpha`. diff --git a/.changeset/red-snakes-float.md b/.changeset/red-snakes-float.md deleted file mode 100644 index f1bd16d84c..0000000000 --- a/.changeset/red-snakes-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Add support for string refs to the `EntityRefLinks` component diff --git a/.changeset/renovate-05696d1.md b/.changeset/renovate-05696d1.md new file mode 100644 index 0000000000..cd9fe676b0 --- /dev/null +++ b/.changeset/renovate-05696d1.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@asyncapi/react-component` to `1.0.0-next.37`. diff --git a/.changeset/renovate-b063c4b.md b/.changeset/renovate-b063c4b.md new file mode 100644 index 0000000000..89292d03a0 --- /dev/null +++ b/.changeset/renovate-b063c4b.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Updated dependency `@codemirror/legacy-modes` to `^0.20.0`. diff --git a/.changeset/renovate-bf43c44.md b/.changeset/renovate-bf43c44.md new file mode 100644 index 0000000000..696017c72b --- /dev/null +++ b/.changeset/renovate-bf43c44.md @@ -0,0 +1,18 @@ +--- +'@backstage/backend-common': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/create-app': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +Updated dependency `fs-extra` to `10.1.0`. diff --git a/.changeset/rich-maps-hear.md b/.changeset/rich-maps-hear.md deleted file mode 100644 index 390da8b6bb..0000000000 --- a/.changeset/rich-maps-hear.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-scaffolder': patch ---- - -Added a new `NextScaffolderRouter` which will eventually replace the exiting router diff --git a/.changeset/rotten-planes-watch.md b/.changeset/rotten-planes-watch.md deleted file mode 100644 index 25aa6b83df..0000000000 --- a/.changeset/rotten-planes-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Handle trailing slashes on GitHub `enterpriseInstanceUrl` settings diff --git a/.changeset/rude-bees-rest.md b/.changeset/rude-bees-rest.md deleted file mode 100644 index 7ca4e7492c..0000000000 --- a/.changeset/rude-bees-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-react': minor ---- - -New search package to hold things the search plugin itself and other frontend plugins (e.g. techdocs, home) depend on. diff --git a/.changeset/rude-toes-compare.md b/.changeset/rude-toes-compare.md deleted file mode 100644 index 3a767ec95b..0000000000 --- a/.changeset/rude-toes-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Added the `metadata.description` to the bottom of each member on the MembersListCard diff --git a/.changeset/search-dry-wolves-join.md b/.changeset/search-dry-wolves-join.md new file mode 100644 index 0000000000..2eb613b642 --- /dev/null +++ b/.changeset/search-dry-wolves-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Fix issue with `HomePageSearchBar` requiring `SearchContext` diff --git a/.changeset/search-heavy-llamas-worry.md b/.changeset/search-heavy-llamas-worry.md new file mode 100644 index 0000000000..2d77d6c4d6 --- /dev/null +++ b/.changeset/search-heavy-llamas-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Export `useSearchContextCheck` hook to check if the search context is available diff --git a/.changeset/selfish-actors-walk.md b/.changeset/selfish-actors-walk.md deleted file mode 100644 index 755f1c3de6..0000000000 --- a/.changeset/selfish-actors-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Exported `IdentityProviders` type. diff --git a/.changeset/seven-deers-rule.md b/.changeset/seven-deers-rule.md new file mode 100644 index 0000000000..6b1976cbfa --- /dev/null +++ b/.changeset/seven-deers-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Prevent orphaned stale indices by permanently marking them for deletion so removal can be re-attempted if it failed previously diff --git a/.changeset/sharp-terms-tap.md b/.changeset/sharp-terms-tap.md deleted file mode 100644 index 72e888a94e..0000000000 --- a/.changeset/sharp-terms-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Adding a name to the core-components Tab styles so can customise in the theme settings diff --git a/.changeset/shiny-students-approve.md b/.changeset/shiny-students-approve.md new file mode 100644 index 0000000000..77de3ffc04 --- /dev/null +++ b/.changeset/shiny-students-approve.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Split `publish:bitbucket` into `publish:bitbucketCloud` and `publish:bitbucketServer`. + +In order to migrate from the deprecated action, you need to replace the use of action +`publish:bitbucket` in your templates with the use of either `publish:bitbucketCloud` +or `publish:bitbucketServer` - depending on which destination SCM provider you use. + +Additionally, these actions will not utilize `integrations.bitbucket` anymore, +but `integrations.bitbucketCloud` or `integrations.bitbucketServer` respectively. +You may or may not have migrated to these already. + +As described in a previous changeset, using these two replacement integrations configs +will not compromise use cases which still rely on `integrations.bitbucket` as this was +set up in a backwards compatible way. + +Additionally, please mind that the option `enableLFS` is only available (and always was) +for Bitbucket Server use cases and therefore, is not even part of the schema for +`publish:bitbucketCloud` anymore. diff --git a/.changeset/short-dodos-sparkle.md b/.changeset/short-dodos-sparkle.md new file mode 100644 index 0000000000..342b8cc22b --- /dev/null +++ b/.changeset/short-dodos-sparkle.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Fix S3 object URL creation at AwsS3EntityProvider by + +- handle absence of region config, +- handle regions with region-less URIs (us-east-1), +- apply URI encoding, +- and simplify the logic overall. diff --git a/.changeset/short-flies-collect.md b/.changeset/short-flies-collect.md new file mode 100644 index 0000000000..d393f92175 --- /dev/null +++ b/.changeset/short-flies-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Handle empty config files gracefully diff --git a/.changeset/short-knives-wave.md b/.changeset/short-knives-wave.md deleted file mode 100644 index a7372618e2..0000000000 --- a/.changeset/short-knives-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -export `locationSpecToLocationEntity` diff --git a/.changeset/silent-bobcats-matter.md b/.changeset/silent-bobcats-matter.md deleted file mode 100644 index f67f2d3ede..0000000000 --- a/.changeset/silent-bobcats-matter.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/config-loader': patch -'@backstage/core-app-api': patch -'@backstage/plugin-catalog-backend-module-bitbucket': patch -'@backstage/plugin-tech-insights-backend': patch ---- - -Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. diff --git a/.changeset/silly-forks-study.md b/.changeset/silly-forks-study.md deleted file mode 100644 index d337aecec6..0000000000 --- a/.changeset/silly-forks-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Updated the dependency on `@backstage/config` to `^1.0.0`. diff --git a/.changeset/silly-llamas-relax.md b/.changeset/silly-llamas-relax.md deleted file mode 100644 index f5a002d46d..0000000000 --- a/.changeset/silly-llamas-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-radar': patch ---- - -Fix an issue where the Radar is not updated when switching between different radars diff --git a/.changeset/silver-drinks-study.md b/.changeset/silver-drinks-study.md deleted file mode 100644 index f6547057f2..0000000000 --- a/.changeset/silver-drinks-study.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-home': patch -'@backstage/plugin-org': patch ---- - -Minor API report cleanup diff --git a/.changeset/silver-panthers-occur.md b/.changeset/silver-panthers-occur.md deleted file mode 100644 index 211e760d09..0000000000 --- a/.changeset/silver-panthers-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fix broken query selectors on techdocs diff --git a/.changeset/silver-pots-call.md b/.changeset/silver-pots-call.md deleted file mode 100644 index 42b3016545..0000000000 --- a/.changeset/silver-pots-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -fix support config ref to use backstage/backstage diff --git a/.changeset/silver-readers-deliver.md b/.changeset/silver-readers-deliver.md new file mode 100644 index 0000000000..ce175f97be --- /dev/null +++ b/.changeset/silver-readers-deliver.md @@ -0,0 +1,8 @@ +--- +'@backstage/cli': patch +'@backstage/codemods': patch +'@backstage/create-app': patch +'@techdocs/cli': patch +--- + +Bump `commander` to version 9.1.0 diff --git a/.changeset/sixty-llamas-change.md b/.changeset/sixty-llamas-change.md new file mode 100644 index 0000000000..a0d183d45c --- /dev/null +++ b/.changeset/sixty-llamas-change.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-search-backend': patch +--- + +Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. diff --git a/.changeset/smart-phones-exist.md b/.changeset/smart-phones-exist.md deleted file mode 100644 index eb1c3d041a..0000000000 --- a/.changeset/smart-phones-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Added a new experimental `repo list-deprecations` command, which scans the entire project for usage of deprecated APIs. diff --git a/.changeset/soft-rice-remember.md b/.changeset/soft-rice-remember.md deleted file mode 100644 index bf207a0dd5..0000000000 --- a/.changeset/soft-rice-remember.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-react': patch ---- - -**BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`. diff --git a/.changeset/sour-cameras-deliver.md b/.changeset/sour-cameras-deliver.md deleted file mode 100644 index 21e23e7833..0000000000 --- a/.changeset/sour-cameras-deliver.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Handle changes to @alpha permission-related types. - -- All exported permission rules and conditions now have a `resourceType`. -- `createCatalogConditionalDecision` now expects supplied conditions to have the appropriate `resourceType`. -- `createCatalogPermissionRule` now expects `resourceType` as part of the supplied rule object. -- Introduce new `CatalogPermissionRule` convenience type. diff --git a/.changeset/sour-lobsters-sniff.md b/.changeset/sour-lobsters-sniff.md deleted file mode 100644 index fb8d1f2076..0000000000 --- a/.changeset/sour-lobsters-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-node': patch ---- - -Fix signature of permission rule in test suites diff --git a/.changeset/spotty-plums-rule.md b/.changeset/spotty-plums-rule.md new file mode 100644 index 0000000000..45d1c8280f --- /dev/null +++ b/.changeset/spotty-plums-rule.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-techdocs': patch +--- + +Removed dependency on `@backstage/core-app-api`. diff --git a/.changeset/stale-carrots-smile.md b/.changeset/stale-carrots-smile.md deleted file mode 100644 index 20b77dd834..0000000000 --- a/.changeset/stale-carrots-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': patch ---- - -Add `isPermission` helper method. diff --git a/.changeset/stale-pigs-reply.md b/.changeset/stale-pigs-reply.md new file mode 100644 index 0000000000..645648c93f --- /dev/null +++ b/.changeset/stale-pigs-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Added a link to the `${GITHUB_TOKEN}` to document how to generate a token diff --git a/.changeset/strange-wasps-whisper.md b/.changeset/strange-wasps-whisper.md deleted file mode 100644 index bef72e276a..0000000000 --- a/.changeset/strange-wasps-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -The logger returned from `getVoidLogger` is now uses a silenced console transport instead. diff --git a/.changeset/strong-mangos-sell.md b/.changeset/strong-mangos-sell.md new file mode 100644 index 0000000000..614d36cf52 --- /dev/null +++ b/.changeset/strong-mangos-sell.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add more common predefined sign-in resolvers to auth providers. + +Add the existing resolver to more providers (already available at `google`): + +- `providers.microsoft.resolvers.emailLocalPartMatchingUserEntityName()` +- `providers.okta.resolvers.emailLocalPartMatchingUserEntityName()` + +Add a new resolver for simple email-to-email matching: + +- `providers.google.resolvers.emailMatchingUserEntityProfileEmail()` +- `providers.microsoft.resolvers.emailMatchingUserEntityProfileEmail()` +- `providers.okta.resolvers.emailMatchingUserEntityProfileEmail()` diff --git a/.changeset/stupid-ways-think.md b/.changeset/stupid-ways-think.md deleted file mode 100644 index 19e68fba90..0000000000 --- a/.changeset/stupid-ways-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed misleading log message during frontend plugin creation. diff --git a/.changeset/swift-bugs-share.md b/.changeset/swift-bugs-share.md new file mode 100644 index 0000000000..c018bdfacf --- /dev/null +++ b/.changeset/swift-bugs-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated the `create-github-app` command to throw an error if the organization argument is a user or a non existing organization. diff --git a/.changeset/swift-fans-shout.md b/.changeset/swift-fans-shout.md deleted file mode 100644 index 7a11b70592..0000000000 --- a/.changeset/swift-fans-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gocd': patch ---- - -Add pagination controls to GoCD build table diff --git a/.changeset/swift-parrots-hammer.md b/.changeset/swift-parrots-hammer.md new file mode 100644 index 0000000000..1b26742937 --- /dev/null +++ b/.changeset/swift-parrots-hammer.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-tasks': patch +--- + +Scheduled tasks now have an optional `scope` field. If unset, or having the +value `'global'`, the old behavior with cross-worker locking is retained. If +having the value `'local'`, there is no coordination across workers and the +behavior is more like `setInterval`. This can be used to replace usages of +`runPeriodically` helpers. diff --git a/.changeset/swift-trees-rest.md b/.changeset/swift-trees-rest.md deleted file mode 100644 index 2e76195997..0000000000 --- a/.changeset/swift-trees-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Added `/validate-entity` endpoint diff --git a/.changeset/tame-cars-press.md b/.changeset/tame-cars-press.md deleted file mode 100644 index d95edad332..0000000000 --- a/.changeset/tame-cars-press.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-techdocs': patch ---- - -Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. diff --git a/.changeset/tasty-beans-clap.md b/.changeset/tasty-beans-clap.md deleted file mode 100644 index f171617860..0000000000 --- a/.changeset/tasty-beans-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-bitbucket': patch ---- - -split BitbucketClient into BitbucketCloudClient, BitbucketServerClient diff --git a/.changeset/tasty-drinks-teach.md b/.changeset/tasty-drinks-teach.md new file mode 100644 index 0000000000..4b4a5a748d --- /dev/null +++ b/.changeset/tasty-drinks-teach.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +--- + +Update to `jose` 4.6.0 diff --git a/.changeset/tasty-emus-hunt.md b/.changeset/tasty-emus-hunt.md deleted file mode 100644 index d3b9430bf4..0000000000 --- a/.changeset/tasty-emus-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': minor ---- - -Expose 'initalFilter' through initialKind prop on Catalog Page. diff --git a/.changeset/tasty-goats-shout.md b/.changeset/tasty-goats-shout.md deleted file mode 100644 index 5b9356122f..0000000000 --- a/.changeset/tasty-goats-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gcalendar': minor ---- - -Fixed issue when not all calendars were fetched for some accounts diff --git a/.changeset/tasty-pears-shake.md b/.changeset/tasty-pears-shake.md deleted file mode 100644 index 565cb02b77..0000000000 --- a/.changeset/tasty-pears-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Updated ProxiedSignInPageProps docs diff --git a/.changeset/techdocs-changeset-not-found.md b/.changeset/techdocs-changeset-not-found.md new file mode 100644 index 0000000000..7ee5ec2b85 --- /dev/null +++ b/.changeset/techdocs-changeset-not-found.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed bugs that prevented a 404 error from being shown when it should have been. diff --git a/.changeset/techdocs-coats-obey.md b/.changeset/techdocs-coats-obey.md deleted file mode 100644 index 0c2c2b5ac2..0000000000 --- a/.changeset/techdocs-coats-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fix permalink scrolling for anchors where the id starts with a number. diff --git a/.changeset/techdocs-fresh-panthers-kick.md b/.changeset/techdocs-fresh-panthers-kick.md deleted file mode 100644 index a35d36a0a8..0000000000 --- a/.changeset/techdocs-fresh-panthers-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. diff --git a/.changeset/techdocs-head-shoulders-knees-toes.md b/.changeset/techdocs-head-shoulders-knees-toes.md deleted file mode 100644 index f96b624897..0000000000 --- a/.changeset/techdocs-head-shoulders-knees-toes.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -Fixed a bug affecting those with cache enabled that would result in empty content being cached if the first attempt to load a static asset from storage were made via a `HEAD` request, rather than a `GET` request. diff --git a/.changeset/techdocs-nice-boats-wonder.md b/.changeset/techdocs-nice-boats-wonder.md new file mode 100644 index 0000000000..434cde841e --- /dev/null +++ b/.changeset/techdocs-nice-boats-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Allow usage of custom tags with sequence node type (such as the !ENV tag) in the techdocs mkdocs config. diff --git a/.changeset/techdocs-parents-suffer.md b/.changeset/techdocs-parents-suffer.md deleted file mode 100644 index 8e366dd506..0000000000 --- a/.changeset/techdocs-parents-suffer.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Some documentation layout tweaks: - -- drawer toggle margins -- code block margins -- sidebar drawer width -- inner content width -- footer link width -- sidebar table of contents scroll diff --git a/.changeset/techdocs-quick-owls-smile.md b/.changeset/techdocs-quick-owls-smile.md new file mode 100644 index 0000000000..db4202fb5b --- /dev/null +++ b/.changeset/techdocs-quick-owls-smile.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': minor +--- + +A package for contributed TechDocs addons. + +In this release it will introduce the ReportIssue addon, which lets you select text and open a GitHub/Gitlab issue. diff --git a/.changeset/techdocs-swift-weeks-admire.md b/.changeset/techdocs-swift-weeks-admire.md deleted file mode 100644 index 8a747a8bb3..0000000000 --- a/.changeset/techdocs-swift-weeks-admire.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@techdocs/cli': minor -'@backstage/plugin-techdocs-backend': minor -'@backstage/plugin-techdocs-node': minor ---- - -BREAKING: The default Techdocs behavior will no longer attempt to copy `docs/README.md` or `README.md` to `docs/index.md` (if not found). To retain this behavior in your instance, you can set the following config in your `app-config.yaml`: - -```yaml -techdocs: - generator: - mkdocs: - legacyCopyReadmeMdToIndexMd: true -``` diff --git a/.changeset/techdocs-touch-screen-wipes.md b/.changeset/techdocs-touch-screen-wipes.md new file mode 100644 index 0000000000..b6467a4232 --- /dev/null +++ b/.changeset/techdocs-touch-screen-wipes.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Updated the return type of `createTechDocsAddonExtension` to better reflect the fact that passing children to Addon components is not a valid use-case. diff --git a/.changeset/thick-bees-brush.md b/.changeset/thick-bees-brush.md new file mode 100644 index 0000000000..4ec068e0a1 --- /dev/null +++ b/.changeset/thick-bees-brush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gcalendar': patch +--- + +fixed google API types dependencies diff --git a/.changeset/thick-comics-fold.md b/.changeset/thick-comics-fold.md deleted file mode 100644 index 5faf2fd399..0000000000 --- a/.changeset/thick-comics-fold.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-tech-insights': patch -'@backstage/plugin-tech-insights-backend': patch ---- - -Improved the Tech-Insights documentation: - -- `lifecycle` examples used `ttl` when it should be `timeToLive` -- Added list of included FactRetrievers -- Added full backend example using all included FactRetrievers -- Added boolean scorecard example image showing results of backend example diff --git a/.changeset/thin-deers-turn.md b/.changeset/thin-deers-turn.md deleted file mode 100644 index d148c3fcdf..0000000000 --- a/.changeset/thin-deers-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -`` now accepts additional props `sidebarOptions` and `submenuOptions` to allow further customization diff --git a/.changeset/thin-lizards-battle.md b/.changeset/thin-lizards-battle.md deleted file mode 100644 index de3658f042..0000000000 --- a/.changeset/thin-lizards-battle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-scaffolder': patch ---- - -Update `usePermission` usage. diff --git a/.changeset/thirty-icons-buy.md b/.changeset/thirty-icons-buy.md deleted file mode 100644 index dd5a9eec19..0000000000 --- a/.changeset/thirty-icons-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Added `ignoreSchemaErrors` to `schema.process`. diff --git a/.changeset/thirty-sloths-knock.md b/.changeset/thirty-sloths-knock.md deleted file mode 100644 index 1ad559e368..0000000000 --- a/.changeset/thirty-sloths-knock.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -The following exports has been moved to `@backstage/plugin-search-react` and will be removed in the next release. import from `@backstage/plugin-search-react` instead. - -- `SearchApi` interface. -- `searchApiRef` -- `SearchContext` -- `SearchContextProvider` -- `useSearch` diff --git a/.changeset/three-rivers-remain.md b/.changeset/three-rivers-remain.md deleted file mode 100644 index d6bfeba093..0000000000 --- a/.changeset/three-rivers-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Added the ability to render extra setting tabs diff --git a/.changeset/tidy-days-warn.md b/.changeset/tidy-days-warn.md deleted file mode 100644 index e84d563f01..0000000000 --- a/.changeset/tidy-days-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Gerrit integration: Added an optional configuration to set the Gitiles base url. diff --git a/.changeset/tidy-emus-stare.md b/.changeset/tidy-emus-stare.md deleted file mode 100644 index 93f3dccde7..0000000000 --- a/.changeset/tidy-emus-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -add gitlab to AnnotateScmSlugEntityProcessor diff --git a/.changeset/tough-forks-carry.md b/.changeset/tough-forks-carry.md new file mode 100644 index 0000000000..88fd82b55d --- /dev/null +++ b/.changeset/tough-forks-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Fix ref to filtered catalog table view when using aggregated relations. diff --git a/.changeset/twelve-birds-wait.md b/.changeset/twelve-birds-wait.md deleted file mode 100644 index 6f71ed2cf7..0000000000 --- a/.changeset/twelve-birds-wait.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Added a connection between www-artist and artist-lookup components - -
- Screenshots - -![www-artist component](https://user-images.githubusercontent.com/74687/161088772-bc1df84e-28c1-4b08-b914-109cd6d04521.jpg) - -![artist-lookup component](https://user-images.githubusercontent.com/74687/161088775-120c5610-f094-4cd1-bb2d-3ab9623a6c72.jpg) - -
diff --git a/.changeset/twelve-boats-count.md b/.changeset/twelve-boats-count.md deleted file mode 100644 index c62309870a..0000000000 --- a/.changeset/twelve-boats-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The `versions:*` commands no longer warns about duplicate plugin libraries, such as `@backstage/plugin-catalog-common`. diff --git a/.changeset/twelve-buses-nail.md b/.changeset/twelve-buses-nail.md deleted file mode 100644 index ddeb7f1413..0000000000 --- a/.changeset/twelve-buses-nail.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/cli': minor ---- - -**BREAKING**: Bump the version range of `jest` from `^26.0.1` to `^27.5.1`. You can find the complete list of breaking changes [here](https://github.com/facebook/jest/releases/tag/v27.0.0). - -We strongly recommend to have completed the [package role migration](https://backstage.io/docs/tutorials/package-role-migration) before upgrading to this version, as the package roles are used to automatically determine the testing environment for each package. If you instead want to set an explicit test environment for each package, you can do so for example in the `"jest"` section in `package.json`. The default test environment for all packages is now `node`, which is also the new Jest default. - -Note that one of the breaking changes of Jest 27 is that the `jsdom` environment no longer includes `setImmediate` and `clearImmediate`, which means you might need to update some of your frontend packages. Another notable change is that `jest.useFakeTimers` now defaults to the `'modern'` implementation, which also mocks microtasks. diff --git a/.changeset/unlucky-schools-heal.md b/.changeset/unlucky-schools-heal.md deleted file mode 100644 index 55daf4e65c..0000000000 --- a/.changeset/unlucky-schools-heal.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-permission-backend': patch ---- - -- Add more specific check for policies which return conditional decisions for non-resource permissions. -- Refine permission validation in authorize endpoint to differentiate between `BasicPermission` and `ResourcePermission` instances. diff --git a/.changeset/unlucky-snakes-turn.md b/.changeset/unlucky-snakes-turn.md deleted file mode 100644 index bcb31feb40..0000000000 --- a/.changeset/unlucky-snakes-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -Fixed possible type error if jenkins response contains null values diff --git a/.changeset/violet-steaks-knock.md b/.changeset/violet-steaks-knock.md new file mode 100644 index 0000000000..66139be422 --- /dev/null +++ b/.changeset/violet-steaks-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Added missing `Routes` element to wrap the `Route` elements of the test app wrapping. diff --git a/.changeset/warm-actors-attend.md b/.changeset/warm-actors-attend.md deleted file mode 100644 index 9b5927dffe..0000000000 --- a/.changeset/warm-actors-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Tweaked `.eslintrc.js` files in the template to avoid having them apply during development. This change does not affect create apps. diff --git a/.changeset/warm-mangos-compete.md b/.changeset/warm-mangos-compete.md deleted file mode 100644 index 3215b36d22..0000000000 --- a/.changeset/warm-mangos-compete.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-permission-common': patch ---- - -- Add more specific `Permission` types. -- Add `createPermission` helper to infer the appropriate type for some permission input. -- Add `isResourcePermission` helper to refine Permissions to ResourcePermissions. diff --git a/.changeset/weak-buttons-play.md b/.changeset/weak-buttons-play.md deleted file mode 100644 index 491b510f77..0000000000 --- a/.changeset/weak-buttons-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gocd': patch ---- - -Add DORA metrics insights to GoCD builds page diff --git a/.changeset/wicked-beds-return.md b/.changeset/wicked-beds-return.md deleted file mode 100644 index add952ba0a..0000000000 --- a/.changeset/wicked-beds-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Fix division by zero in currentToDeclaredResourceToPerc when pod resources weren't set diff --git a/.changeset/wicked-feet-clap.md b/.changeset/wicked-feet-clap.md deleted file mode 100644 index e27f641820..0000000000 --- a/.changeset/wicked-feet-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Updated the visibility of database connection fields in config to be secret diff --git a/.changeset/wild-emus-film.md b/.changeset/wild-emus-film.md deleted file mode 100644 index abb77c5ddc..0000000000 --- a/.changeset/wild-emus-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-insights': minor ---- - -Add checksId option to EntityTechInsightsScorecardContent component diff --git a/.changeset/wild-pigs-work.md b/.changeset/wild-pigs-work.md new file mode 100644 index 0000000000..462b3b7233 --- /dev/null +++ b/.changeset/wild-pigs-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated the rendering of AsyncApi definitions to be able to resolve absolute HTTP $ref references. diff --git a/.changeset/wise-emus-wait.md b/.changeset/wise-emus-wait.md new file mode 100644 index 0000000000..6f0e5348c7 --- /dev/null +++ b/.changeset/wise-emus-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +The internal elements created as part of the `mountedRoutes` implementation are now hidden during rendering. diff --git a/.changeset/witty-years-sniff.md b/.changeset/witty-years-sniff.md deleted file mode 100644 index f8d8acf5c1..0000000000 --- a/.changeset/witty-years-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-stack-overflow': minor ---- - -Add stack overflow plugin diff --git a/.changeset/yellow-hats-remember.md b/.changeset/yellow-hats-remember.md deleted file mode 100644 index 1a9064f94e..0000000000 --- a/.changeset/yellow-hats-remember.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -Make `resourceRef` required in `JenkinsApi` to match usage. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 202d7ecd14..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: '/' - schedule: - interval: daily - time: '04:00' - open-pull-requests-limit: 10 - labels: - - dependencies - - package-ecosystem: npm - directory: '/microsite/' - schedule: - interval: daily - time: '04:00' - open-pull-requests-limit: 5 - labels: - - dependencies - - package-ecosystem: npm - directory: '/storybook/' - schedule: - interval: daily - time: '04:00' - open-pull-requests-limit: 5 - labels: - - dependencies diff --git a/.github/renovate.json5 b/.github/renovate.json5 index aaf292f8cc..ff351a5e0f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -2,4 +2,18 @@ labels: ['dependencies'], extends: ['config:base', ':disableDependencyDashboard', ':gitSignOff'], rangeStrategy: 'update-lockfile', + // @elastic/elasticsearch is ignored due to licensing issues. See #10992 + ignoreDeps: ['@elastic/elasticsearch'], + packageRules: [ + { + matchSourceUrlPrefixes: ['https://github.com/spotify/web-scripts'], + groupName: 'Spotify web-scripts monorepo packages', + rangeStrategy: 'replace', + }, + { + matchSourceUrlPrefixes: ['https://github.com/microsoft/rushstack'], + groupName: 'API Extractor / Rush Stack monorepo packages', + rangeStrategy: 'replace', + }, + ], } diff --git a/.github/styles/vocab.txt b/.github/vale/Vocab/Backstage/accept.txt similarity index 98% rename from .github/styles/vocab.txt rename to .github/vale/Vocab/Backstage/accept.txt index 72864d2c9f..caf27d4593 100644 --- a/.github/styles/vocab.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -8,9 +8,6 @@ airbrake Anddddd Apdex api -Api -apis -args asciidoc async Atlassian @@ -19,7 +16,6 @@ autoscaling Autoscaling autoselect Avro -aws backported backporting Bigtable @@ -38,7 +34,6 @@ changesets Changesets chanwit Chanwit -ci CI/CD classname cli @@ -108,6 +103,7 @@ Firekube Firestore Fiverr Francesco +gerrit Gerrit gitbeaker github @@ -128,7 +124,6 @@ hoc horizontalpodautoscalers Hostname hotspots -html http https Iain @@ -315,7 +310,6 @@ templaters Templaters theia thumbsup -toc todo tolerations Tolerations @@ -323,7 +317,7 @@ toolchain toolsets tooltip tooltips -touchpoints +touchpoint transpilation transpiled transpiler @@ -340,7 +334,7 @@ unregistration untracked upsert upvote -url +URIs URLs utils validator diff --git a/.github/vale/config.ini b/.github/vale/config.ini new file mode 100644 index 0000000000..36acb2c058 --- /dev/null +++ b/.github/vale/config.ini @@ -0,0 +1,6 @@ +StylesPath = . +Vocab = Backstage + +[*.md] +BasedOnStyles = Vale +Vale.Terms = NO diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 796df14ac7..31a315d044 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,7 @@ jobs: if: ${{ steps.yarn-lock.outcome == 'success' }} run: yarn lerna -- run test --since origin/master -- --coverage --runInBand env: + BACKSTAGE_NEXT_TESTS: 1 BACKSTAGE_TEST_DISABLE_DOCKER: 1 BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} @@ -166,6 +167,7 @@ jobs: yarn lerna -- run test -- --coverage --runInBand bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD) env: + BACKSTAGE_NEXT_TESTS: 1 BACKSTAGE_TEST_DISABLE_DOCKER: 1 BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index d5d3659625..85668bdc3a 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -79,7 +79,7 @@ jobs: - name: Discord notification if: ${{ failure() }} - uses: Ilshidur/action-discord@0.2.0 + uses: Ilshidur/action-discord@0.3.2 env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index fd9ed216be..bcb4016ab9 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -122,6 +122,7 @@ jobs: bash <(curl -s https://codecov.io/bash) -f packages/core-components/coverage/* -F core-components bash <(curl -s https://codecov.io/bash) -f packages/core-plugin-api/coverage/* -F core-plugin-api env: + BACKSTAGE_NEXT_TESTS: 1 BACKSTAGE_TEST_DISABLE_DOCKER: 1 BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} @@ -129,7 +130,7 @@ jobs: - name: Discord notification if: ${{ failure() }} - uses: Ilshidur/action-discord@0.2.0 + uses: Ilshidur/action-discord@0.3.2 env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: @@ -220,7 +221,7 @@ jobs: # Notify everyone about this great new release :D - name: Discord notification - uses: Ilshidur/action-discord@0.2.0 + uses: Ilshidur/action-discord@0.3.2 env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} TAG_NAME: ${{ steps.create_tag.outputs.tag_name }} diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 75c154792c..c1a4b9e1d0 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -43,7 +43,7 @@ jobs: # End of yarn setup - name: Run Prettier on ADOPTERS.md - uses: creyD/prettier_action@v3.1 + uses: creyD/prettier_action@v3.3 with: # Modifies commit only if prettier autofixed the ADOPTERS.md prettier_options: --config docs/prettier.config.js --write ADOPTERS.md diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 58d715c5a8..a6a544e6a5 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -11,11 +11,12 @@ jobs: steps: - uses: actions/checkout@v2 with: + fetch-depth: 0 token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Install Dependencies run: yarn --frozen-lockfile - name: Create Release Pull Request - uses: changesets/action@master + uses: backstage/changesets-action@v2 with: # Calls out to `changeset version`, but also runs prettier version: yarn release diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 55c1c23587..ad2e92799f 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -12,10 +12,18 @@ jobs: steps: - uses: actions/checkout@v2 + + # Vale does not support file excludes, so we use the script to generate a list of files instead + # The action also does not allow args or a local config file to be passed in, so the files array + # also contains an "--config=.github/vale/config.ini" option + - name: generate vale args + id: generate + run: echo "::set-output name=args::$(node scripts/check-docs-quality.js --ci-args)" + - name: documentation quality check - uses: errata-ai/vale-action@v1.4.0 - # Whitelist excluding ADOPTERS, CHANGELOG and OWNERS (no exclude flag exists) + uses: errata-ai/vale-action@v1.5.0 with: - files: '[".changeset", ".github", "contrib", "docs", "microsite", "packages", "plugins", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "GOVERNANCE.md", "README.md"]' + # This also contains --config=.github/vale/config.ini ... :/ + files: '${{ steps.generate.outputs.args }}' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 8dc621ebf7..becff56953 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -44,7 +44,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 + uses: microsoft/setup-msbuild@v1.0.3 - name: setup chrome uses: browser-actions/setup-chrome@latest - name: yarn install diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 6899b6ec2b..7156bec4f6 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -58,6 +58,7 @@ jobs: - name: test run: yarn lerna -- run test env: + BACKSTAGE_NEXT_TESTS: 1 BACKSTAGE_TEST_DISABLE_DOCKER: 1 # credit: https://github.com/appleboy/discord-action/issues/3#issuecomment-731426861 diff --git a/ADOPTERS.md b/ADOPTERS.md index 1a1c3686d4..845dd1b208 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -26,7 +26,7 @@ _If you're using Backstage in your organization, please try to add your company | [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | | [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | | [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Matt Waldron](https://github.com/daftgopher) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [Peloton](https://www.onepeloton.com/) | [Matt Waldron](https://github.com/daftgopher) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | | [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | | [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | | [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | @@ -34,7 +34,7 @@ _If you're using Backstage in your organization, please try to add your company | [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | | [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | | [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | | [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | | [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | | [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | @@ -113,3 +113,12 @@ _If you're using Backstage in your organization, please try to add your company | [SIX](https://www.six-group.com/) | [@jbadeau](https://github.com/jbadeau), [@tomassatka](https://github.com/tomassatka) | Internal DevOps portal hosting our software and dataset catalog, as well as custom plugins for observability, service virtualization, deployments, incident managment and quality metrics. | | [Raiffeisen Bank International](https://www.rbinternational.com/) | [Daniel Baumgartner](https://github.com/dabarbi) | From developers for developers: software catalog, techdocs and heavy use of scaffolder to drive reuse on engineering level forward. Part of inner source initiative. Multi national setup coming. | | [Spread Group](https://www.spreadgroup.com/) | [Luna Stadler](https://github.com/heyLu), [Iván González](https://github.com/ivangonzalezacuna) | Internal Developer Portal, an overview of all running software, architecture documentation and more; replacing and unifying a variety of internal tools. | +| [RD Station](https://rdstation.com) | [Rogerio Angeliski](https://github.com/angeliski), [Paula Assis](https://github.com/paulassis), [Guilherme Eric](https://github.com/guilhermeeric), [Daniela Adamatti](https://github.com/daniadamatti), [Luana Negreiros](https://github.com/luananegreiros) | Developer portal, scaffolding, services catalog. We are looking to centralize automations and information for the whole engineering team . | +| [Resuelve Tu Deuda](https://resuelve.mx) | [Iván Álvarez](https://github.com/ivanhoe), [Jorge Medina](https://github.com/jorgearma1982) | Internal developer portal, service catalog, tech docs, api doc | +| [Pachama](https://pachama.com/) | [Aron Gates](https://github.com/agates4) | Internal Developer Portal, a catalog of all microservices, architecture documentation, and templates to generate developer resources. | +| [SEEK](https://www.seek.com.au) | [Jahred Hope](https://github.com/jahredhope) | Developer portal for developer tooling and technical documentation. | +| [Marks & Spencer](https://www.marksandspencer.com/) | [Kamal Cheriyath](https://github.com/kcheriyath) | Centralised discovery, adoption and devops automation hub for Engineering & Architecture. | +| [McKesson](https://www.mckesson.com/) | [Agnel Antony](https://github.com/aantony2) | Internal Developer Platform for developer gated CI/CD templates, technical documentation, cloud automation service catalog, etc. | +| [World Fuel Services](https://www.wfscorp.com/) | [Anirudh Kurapathi](https://github.com/anirudhkurapati), [Alex Kwon](https://github.com/alexkwon), [Lester Hernandez](https://github.com/lhernandez-wfscorp), [Avi Boru](https://github.com/aviboru), [Vardhan Annapureddy](https://github.com/harshaaws) | Internal Developer Portal, service catalog product, API's, Software Templates, tech docs and more. | +| [leboncoin](https://www.leboncoin.fr/) | [Andy Ladjadj](https://github.com/aladjadj) | Centralize our multiple UI in a single portal. Simplify onbording, new features and harmonize how people search information. Core features (catalog,api,docs,scafolder) are good to start the adoption. status: internal beta. | +| [Contentful](https://www.contentful.com) | [James Bourne](https://github.com/jamesmbourne) | Centralized documentation of service ownership, APIs, and documentation, and new service creation with a custom scaffolder - [full case study with Roadie](https://roadie.io/case-studies/maintaining-velocity-through-hypergrowth-contentful/). | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4aad09207..ca7af55d3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ If you're contributing to the backend or CLI tooling, be mindful of cross-platfo Also be sure to skim through our [ADRs](docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for. -If there are any updates in `markdown` file please make sure to run `yarn run lint:docs`. Though it is checked on `lint-staged`. It is required to install [vale](https://docs.errata.ai/vale/install) `1.4.0` separately and make sure it is accessed by global command. +If there are any updates in `markdown` file please make sure to run `yarn run lint:docs`. Though it is checked on `lint-staged`. It is required to install [vale](https://docs.errata.ai/vale/install) separately and make sure it is accessed by global command. ## Developer Certificate of Origin diff --git a/STYLE.md b/STYLE.md index 391b31ce50..b28c5da4fa 100644 --- a/STYLE.md +++ b/STYLE.md @@ -146,7 +146,7 @@ There are a few things to pay attention to, in order to make the documentation s API documenter will not recognize arrow functions as functions, but rather as a constant that shows up in the list of exported variables. By declaring functions using the `function` keyword, they will show up in the list of functions. They will also get a much nicer documentation page for the individual function that shows information about parameters and return types. -This also extends to React components, since API documenter doesn't have any special handling of those. By always defining exported React components using the `function` keyword, we make them show up among the list of functions in the API reference, where they are then easily discoverable through the `(props)` args (which you should be sure to include!). +This also extends to React components, since API documenter doesn't have any special handling of those. By always defining exported React components using the `function` keyword, we make them show up among the list of functions in the API reference, where they are then easily discoverable through the `(props)` argument (which you should be sure to include!). ![image](https://user-images.githubusercontent.com/4984472/133120461-59d74c3e-ebd9-44f9-900d-cc30f54a3cd2.png) diff --git a/contrib/chart/backstage/Chart.lock b/contrib/chart/backstage/Chart.lock index e390bf21ba..d18a48a655 100644 --- a/contrib/chart/backstage/Chart.lock +++ b/contrib/chart/backstage/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: postgresql repository: https://charts.bitnami.com/bitnami - version: 9.4.1 -digest: sha256:f949ec0fe7d146610ce2ee78fbfb4e52d235883a797968b0a1e61aa88ada2786 -generated: "2020-09-25T08:59:54.255582519+02:00" + version: 9.8.12 +digest: sha256:549b9a0cdf7b2e0ad949ebad853a467bf320928970a946fb0ef7e13e9bdb7a10 +generated: "2022-04-21T07:02:48.301491565Z" diff --git a/contrib/chart/backstage/Chart.yaml b/contrib/chart/backstage/Chart.yaml index 21f7084415..9770ef665c 100644 --- a/contrib/chart/backstage/Chart.yaml +++ b/contrib/chart/backstage/Chart.yaml @@ -18,7 +18,7 @@ sources: dependencies: - name: postgresql condition: postgresql.enabled - version: 9.4.1 + version: 9.8.12 repository: https://charts.bitnami.com/bitnami maintainers: diff --git a/contrib/docker/cookiecutter-with-jinja2-extensions/Dockerfile b/contrib/docker/cookiecutter-with-jinja2-extensions/Dockerfile index 4f5f72bdf8..b854a00675 100644 --- a/contrib/docker/cookiecutter-with-jinja2-extensions/Dockerfile +++ b/contrib/docker/cookiecutter-with-jinja2-extensions/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.7 +FROM alpine:3.15 RUN apk add --update \ git \ diff --git a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md index 8a4d6fb273..9ec3f1ca64 100644 --- a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md +++ b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md @@ -64,8 +64,8 @@ if (process.env.HTTPS_PROXY) { If your development environment is in the cloud (like with [AWS Cloud9](https://aws.amazon.com/cloud9/) or an instance of [Theia](https://theia-ide.org/)), you will need to update your configuration. -You will probably need to make some changes in `app-config.yaml` (or another config file like `app-config.local.yaml` if you've created it, see the [configuration doc](https://backstage.io/docs/conf/#supplying-configuration)). -The exact values will depend on your setup but for instance, if your public url is `https://your-public-url.com` and the port `3000` and `8080` are open: +You will probably need to make some changes in `app-config.yaml` (or another config file like `app-config.local.yaml` if you've created it, see the [configuration doc](https://backstage.io/docs/conf/#supplying-configuration)). +The exact values will depend on your setup but for instance, if your public URL is `https://your-public-url.com` and the port `3000` and `8080` are open: ```yaml app: diff --git a/cypress/.npmrc b/cypress/.npmrc new file mode 100644 index 0000000000..c3c66347fd --- /dev/null +++ b/cypress/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +engine-strict=true diff --git a/cypress/.yarnrc b/cypress/.yarnrc new file mode 100644 index 0000000000..a2c82b2833 --- /dev/null +++ b/cypress/.yarnrc @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +registry "https://registry.npmjs.org/" +network-timeout 300000 diff --git a/cypress/README.md b/cypress/README.md index 2bb310e300..15338fe371 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -18,6 +18,17 @@ yarn install yarn cypress run ``` +Note that the tests expect to run against a built application, so you'll want to +run a Docker image and either create an `app-config.cypress.yaml` or pass the +necessary environment variables: + +```sh +yarn tsc +yarn build +yarn workspace example-backend build-image +docker run -p 7007:7007 example-backend +``` + You can open up the `cypress` console by using `yarn cypress open`. You can also run towards any Backstage installation by using the Cypress Environment Variable overrides. diff --git a/cypress/fixtures/mkdocs.yml b/cypress/fixtures/mkdocs.yml index 2362fab898..071d147509 100644 --- a/cypress/fixtures/mkdocs.yml +++ b/cypress/fixtures/mkdocs.yml @@ -1,5 +1,8 @@ site_name: e2e Fixture Documentation site_description: Documentation used for end-to-end tests of TechDocs in Backstage. +repo_url: https://github.com/backstage/backstage +edit_uri: edit/master/cypress/fixtures/docs + nav: - Home: index.md - Sub-page 1: sub-page-one.md diff --git a/cypress/yarn.lock b/cypress/yarn.lock index f301e87d3a..bb9aaecb89 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -2,15 +2,10 @@ # yarn lockfile v1 -"@cypress/listr-verbose-renderer@^0.4.1": - version "0.4.1" - resolved "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" - integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= - dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cypress/request@^2.88.5": version "2.88.10" @@ -44,81 +39,74 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" - integrity sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ== - dependencies: - any-observable "^0.3.0" +"@types/node@*": + version "17.0.25" + resolved "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== "@types/node@^14.14.31": - version "14.17.0" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.0.tgz#3ba770047723b3eeb8dc9fca02cce8a7fb6378da" - integrity sha512-w8VZUN/f7SSbvVReb9SWp6cJFevxb4/nkG65yLAya//98WgocKm5PLDAtSs5CtJJJM+kHmJjO/6mmYW4MHShZA== + version "14.18.13" + resolved "https://registry.npmjs.org/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277" + integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA== "@types/sinonjs__fake-timers@^6.0.2": - version "6.0.2" - resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" - integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + version "6.0.4" + resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d" + integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A== "@types/sizzle@^2.3.2": - version "2.3.2" - resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + version "2.3.3" + resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== -ansi-escapes@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== dependencies: - color-convert "^1.9.0" + "@types/node" "*" -ansi-styles@^4.1.0: +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - arch@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" @@ -127,10 +115,15 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + version "3.2.3" + resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -153,9 +146,9 @@ aws4@^1.8.0: integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== bcrypt-pbkdf@^1.0.0: version "1.0.2" @@ -187,11 +180,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - cachedir@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -202,30 +190,10 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.0.0, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -235,54 +203,39 @@ check-more-types@^2.24.0: resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= -ci-info@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.1.1.tgz#9a32fcefdf7bcdb6f0a7e1c0f8098ec57897b80a" - integrity sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ== +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== -cli-cursor@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= - dependencies: - restore-cursor "^1.0.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^2.0.0, cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - restore-cursor "^2.0.0" + restore-cursor "^3.1.0" cli-table3@~0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" - integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + version "0.6.2" + resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== dependencies: - object-assign "^4.1.0" string-width "^4.2.0" optionalDependencies: - colors "^1.1.2" + "@colors/colors" "1.5.0" -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" + slice-ansi "^3.0.0" + string-width "^4.2.0" color-convert@^2.0.1: version "2.0.1" @@ -291,20 +244,15 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colors@^1.1.2: - version "1.4.0" - resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +colorette@^2.0.16: + version "2.0.16" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" @@ -319,26 +267,16 @@ commander@^5.1.0: integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + version "1.8.2" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -353,11 +291,10 @@ cross-spawn@^7.0.0: which "^2.0.1" cypress@^7.3.0: - version "7.3.0" - resolved "https://registry.npmjs.org/cypress/-/cypress-7.3.0.tgz#17345b8d18681c120f033e7d8fd0f0271e9d0d51" - integrity sha512-aseRCH1tRVCrM6oEfja6fR/bo5l6e4SkHRRSATh27UeN4f/ANC8U7tGIulmrISJVy9xuOkOdbYKbUb2MNM+nrw== + version "7.7.0" + resolved "https://registry.npmjs.org/cypress/-/cypress-7.7.0.tgz#0839ae28e5520536f9667d6c9ae81496b3836e64" + integrity sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ== dependencies: - "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" "@cypress/xvfb" "^1.2.4" "@types/node" "^14.14.31" @@ -369,21 +306,24 @@ cypress@^7.3.0: cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" + cli-cursor "^3.1.0" cli-table3 "~0.6.0" commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.10.4" - debug "4.3.2" + debug "^4.3.2" + enquirer "^2.3.6" eventemitter2 "^6.4.3" execa "4.1.0" executable "^4.1.1" - extract-zip "^1.7.0" + extract-zip "2.0.1" + figures "^3.2.0" fs-extra "^9.1.0" getos "^3.2.1" is-ci "^3.0.0" is-installed-globally "~0.4.0" lazy-ass "^1.6.0" - listr "^0.14.3" + listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" minimist "^1.2.5" @@ -404,29 +344,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== - dayjs@^1.10.4: - version "1.10.4" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" - integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== - -debug@4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@^2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" + version "1.11.1" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz#90b33a3dda3417258d48ad2771b415def6545eb0" + integrity sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== debug@^3.1.0: version "3.2.7" @@ -435,6 +356,13 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -448,11 +376,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -465,15 +388,22 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= eventemitter2@^6.4.3: - version "6.4.4" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" - integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== + version "6.4.5" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" + integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw== execa@4.1.0: version "4.1.0" @@ -497,25 +427,21 @@ executable@^4.1.1: dependencies: pify "^2.2.0" -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= - extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extract-zip@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" + debug "^4.1.1" + get-stream "^5.1.0" yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" extsprintf@1.3.0: version "1.3.0" @@ -523,9 +449,9 @@ extsprintf@1.3.0: integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + version "1.4.1" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fd-slicer@~1.1.0: version "1.1.0" @@ -534,18 +460,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -figures@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" @@ -578,7 +496,7 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -get-stream@^5.0.0: +get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== @@ -600,9 +518,9 @@ getpass@^0.1.1: assert-plus "^1.0.0" glob@^7.1.3: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -619,21 +537,9 @@ global-dirs@^3.0.0: ini "2.0.0" graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.5" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" - integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + version "4.2.10" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-flag@^4.0.0: version "4.0.0" @@ -654,10 +560,10 @@ human-signals@^1.1.1: resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== inflight@^1.0.4: version "1.0.6" @@ -667,7 +573,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -678,23 +584,11 @@ ini@2.0.0: integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== is-ci@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" - integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== + version "3.0.1" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== dependencies: - ci-info "^3.1.1" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + ci-info "^3.2.0" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -709,42 +603,25 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" - is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-promise@^2.1.0: - version "2.2.2" - resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== isexe@^2.0.0: version "2.0.0" @@ -795,49 +672,19 @@ lazy-ass@^1.6.0: resolved "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== - dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" lodash.once@^4.1.1: version "4.1.1" @@ -849,50 +696,40 @@ lodash@^4.17.21: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - log-symbols@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" - integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + version "4.1.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - chalk "^4.0.0" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" -log-update@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: - ansi-escapes "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -mime-db@1.45.0: - version "1.45.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.28" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.45.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" @@ -900,9 +737,9 @@ mimic-fn@^2.1.0: integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -911,18 +748,6 @@ minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -mkdirp@^0.5.4: - version "0.5.5" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -940,16 +765,6 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -957,18 +772,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.0: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -981,10 +784,12 @@ ospath@^1.2.2: resolved "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" path-is-absolute@^1.0.0: version "1.0.1" @@ -1016,11 +821,6 @@ pretty-bytes@^5.6.0: resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - psl@^1.1.28: version "1.8.0" resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -1045,9 +845,9 @@ punycode@^2.1.1: integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== qs@~6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystring@0.2.0: version "0.2.0" @@ -1055,22 +855,9 @@ querystring@0.2.0: integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= ramda@~0.27.1: - version "0.27.1" - resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" - integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== - -readable-stream@^2.2.2: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" + version "0.27.2" + resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" + integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== request-progress@^3.0.0: version "3.0.0" @@ -1079,22 +866,19 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" + onetime "^5.1.0" signal-exit "^3.0.2" +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1102,23 +886,18 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" -rxjs@^6.3.3: - version "6.6.3" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== +rxjs@^7.5.1: + version "7.5.5" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== dependencies: - tslib "^1.9.0" + tslib "^2.1.0" safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -1137,19 +916,32 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" sshpk@^1.14.1: - version "1.16.1" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + version "1.17.0" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -1161,77 +953,27 @@ sshpk@^1.14.1: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1246,16 +988,16 @@ supports-color@^8.1.1: dependencies: has-flag "^4.0.0" -symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - throttleit@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= +through@^2.3.8: + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + tmp@~0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -1271,10 +1013,10 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== tunnel-agent@^0.6.0: version "0.6.0" @@ -1288,10 +1030,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== typescript@^4.1.3: version "4.1.3" @@ -1316,11 +1058,6 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -1342,13 +1079,23 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" wrappy@1: version "1.0.2" diff --git a/docs/assets/techdocs/addon-locations.png b/docs/assets/techdocs/addon-locations.png new file mode 100644 index 0000000000..e535fe61aa Binary files /dev/null and b/docs/assets/techdocs/addon-locations.png differ diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 07d6869fe3..282088fef9 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -74,7 +74,7 @@ export class ProviderAAuthProvider implements OAuthProviderHandlers { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, - passReqToCallback: false as true, + passReqToCallback: false, response_type: 'code', /// ... etc } @@ -230,7 +230,7 @@ name. ### Test the new provider You can `curl -i localhost:7007/api/auth/providerA/start` and which should -provide a `302` redirect with a `Location` header. Paste the url from that +provide a `302` redirect with a `Location` header. Paste the URL from that header into a web browser and you should be able to trigger the authorization flow. diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 14b99bfea5..a8c6020cae 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -50,7 +50,7 @@ The GitHub provider is a structure with three configuration keys: - `clientSecret`: The client secret tied to the generated client ID. - `enterpriseInstanceUrl` (optional): The base URL for a GitHub Enterprise instance, e.g. `https://ghe..com`. Only needed for GitHub Enterprise. -- `callbackUrl` (optional): The callback url that GitHub will use when +- `callbackUrl` (optional): The callback URL that GitHub will use when initiating an OAuth flow, e.g. `https://your-intermediate-service.com/handler`. Only needed if Backstage is not the immediate receiver (e.g. one OAuth app for many backstage instances). diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index fea82894c8..686c9a9dab 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -1,77 +1,123 @@ --- id: identity-resolver -title: Identity resolver -description: Identity resolvers of Backstage users after they sign-in +title: Sign-in Identities and Resolvers +description: An introduction to Backstage user identities and sign-in resolvers --- -This guide explains how the identity of a Backstage user is stored inside their -Backstage Identity Token and how you can customize the Sign-In resolvers to -include identity and group membership information of the user from other -external systems. This ultimately helps with determining the ownership of a -Backstage entity by a user. The ideas here were originally proposed in the RFC -[#4089](https://github.com/backstage/backstage/issues/4089). +By default, every Backstage auth provider is configured only for the use-case of +access delegation. This enables Backstage to request resources and actions from +external systems on behalf of the user, for example re-triggering a build in CI. -When a user signs in to Backstage, inside the `claims` field of their Backstage -Token (which are standard JWT tokens) a special `ent` claim is set. `ent` -contains a list of -[entity references](../features/software-catalog/references.md), each of which -denotes an identity or a membership that is relevant to the user. There is no -guarantee that these correspond to actual existing catalog entities. +If you want to use an auth provider to sign in users, you need to explicitly configure +it have sign-in enabled and also tell it how the external identities should +be mapped to user identities within Backstage. -Let's take an example sign-in resolver for the Google auth provider and explore -how the `ent` field inside `claims` can be set. +## Backstage User Identity -Inside your `packages/backend/src/plugins/auth.ts` file, you can provide custom -sign-in resolvers and set them for any of the Authentication providers inside -`providerFactories` of the `createRouter` imported from the -`@backstage/plugin-auth-backend` plugin. +A user identity within Backstage is built up from two pieces of information, a +user [entity reference](../features/software-catalog/references.md), and a +set of ownership entity references. +When a user signs in, a Backstage token is generated with these two pieces of information, +which is then used to identity the user within the Backstage ecosystem. + +The user entity reference should uniquely identify the logged in user in Backstage. +It is encouraged that a matching user entity also exists within the Software Catalog, +but it is not required. If the user entity exists in the catalog it can be used to +store additional data about the user. There may even be some plugins that require +this for them to be able to function. + +The ownership references are also entity references, and it is likewise +encouraged that these entities exist within the catalog, but it is not a requirement. +The ownership references are used to determine what the user owns, as a set +of references that the user claims ownership though. For example, a user +Jane (`user:default/jane`) might have the ownership references `user:default/jane`, +`group:default/team-a`, and `group:default/admins`. Given these ownership claims, +any entity that is marked as owned by either of `user:jane`, `team-a`, or `admins` would +be considered owned by Jane. + +The ownership claims often contain the user entity reference itself, but it is not +required. It is also worth noting that the ownership claims can also be used to +resolve other relations similar to ownership, such as a claim for a `maintainer` or +`operator` status. + +The Backstage token that encapsulates the user identity is a JWT. The user entity +reference is stored in the `sub` claim of the payload, while the ownership references +are stored in a custom `ent` claim. Both the user and ownership references should +always be full entity references, as opposed to shorthands like just `jane` or `user:jane`. + +## Sign-in Resolvers + +Signing in a user into Backstage requires a mapping of the user identity from the +third-party auth provider to a Backstage user identity. This mapping can vary quite +a lot between different organizations and auth providers, and because of that there's +no default way to resolve user identities. The auth provider that one wants to use +for sign-in must instead be configured with a sign-in resolver, which is a function +that is responsible for creating this user identity mapping. + +The input to the sign-in resolver function is the result of a successful log in with +the given auth provider, as well as a context object that contains various helpers +for looking up users and issuing tokens. There are also a number of built-in sign-in +resolvers that can be used, which are covered a bit further down. + +### Custom Resolver Example + +Let's look at an example of a custom sign-in resolver for the Google auth provider. +This all typically happens within your `packages/backend/src/plugins/auth.ts` file, +which is responsible for setting up and configuring the auth backend plugin. + +You provide the resolver as part of the options you pass when creating a new auth +provider factory. This means you need to replace the default Google provider with +one that you create. Be sure to also include the existing `defaultAuthProviderFactories` +if you want to keep all of the built-in auth providers installed. + +Now let's look at the example, with the rest of the commentary being made with in +the code comments: ```ts -import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, ): Promise { return await createRouter({ - ... + ...env, providerFactories: { - google: createGoogleProvider({ + ...defaultAuthProviderFactories, + google: providers.google.create({ signIn: { - resolver: async ({ profile: { email } }, ctx) => { - // Call a custom validator function that checks that the email is - // valid and on our own company's domain, and throws an Error if it - // isn't. - // TODO: Implement this function - validateEmail(email); + resolver: async (info, ctx) => { + const { + profile: { email }, + } = info; + // Profiles are not always guaranteed to to have an email address. + // You can also find more provider-specific information in `info.result`. + // It typically contains a `fullProfile` object as well as ID and/or access + // tokens that you can use for additional lookups. + if (!email) { + throw new Error('User profile contained no email'); + } - // List of entity references that denote the identity and - // membership of the user - const ent: string[] = []; + // You can add your own custom validation logic here. + // Logins can be prevented by throwing an error like the one above. + myEmailValidator(email); - // Let's use the username in the email ID as the user's default - // unique identifier inside Backstage. - const [id] = email.split('@'); - const userEntityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, + // This example resolver simply uses the local part of the email as the name. + const [name] = email.split('@'); + + // This helper function handles sign-in by looking up a user in the catalog. + // The lookup can be done either by reference, annotations, or custom filters. + // + // The helper also issues a token for the user, using the standard group + // membership logic to determine the ownership references of the user. + return ctx.signInWithCatalogUser({ + entityRef: { name }, }); - ent.push(userEntityRef); - - // Let's call the internal LDAP provider to get a list of groups - // that the user belongs to, and add those to the list as well - const ldapGroups = await getLdapGroups(email); - ldapGroups.forEach(group => ent.push(stringifyEntityRef({ - kind: 'Group', - namespace: DEFAULT_NAMESPACE, - name: group, - }))); - - // Issue the token containing the entity claims - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: userEntityRef, ent }, - }); - return { id, token }; }, }, }), @@ -80,97 +126,118 @@ export default async function createPlugin( } ``` -As you can see, the generated Backstage Token now contains all the claims about -the identity and membership of the user. Once the sign-in process is complete, -and we need to find out if a user owns an Entity in the Software Catalog, these -`ent` claims can be used to determine the ownership. +### Built-in Resolvers -According to the RFC, the definition of the ownership of an entity E, for a user -U, is as follows: - -- Get all the `ownedBy` relations of E, and call them O -- Get all the claims of the user U and call them C -- If any C matches any O, return `true` -- Get all Group entities that U is a member of, using the regular - `memberOf`/`hasMember` relation mechanism, and call them G -- If any G matches any O, return `true` -- Otherwise, return `false` - -## Default sign-in resolvers - -Of course you don't have to customize the sign-in resolver if you don't need to. -The Auth backend plugin comes with a set of default sign-in resolvers which you -can use. For example - the Google provider has a default email-based sign-in -resolver, which will search the catalog for a single user entity that has a -matching `google.com/email` annotation. - -It can be enabled like this +You don't always have to write your own custom resolver. The auth backend plugin provides +build-in resolvers for many of the common sign-in patterns. You access these via the `resolvers` +property of each of the auth provider integrations. For example, the Google provider has +a built in resolver that works just like the one we defined above: ```ts -// File: packages/backend/src/plugins/auth.ts -import { googleEmailSignInResolver, createGoogleProvider } from '@backstage/plugin-auth-backend'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - ... - providerFactories: { - google: createGoogleProvider({ - signIn: { - resolver: googleEmailSignInResolver - } -... +providers.google.create({ + signIn: { + resolver: providers.google.resolvers.emailLocalPartMatchingUserEntityName(), + }, +}); ``` -## Resolving membership through the catalog - -If you want to provide additional claims through Sign-In resolvers but still -have the software catalog handle group (and transitive group) membership, you -can do this using the `CatalogIdentityClient` provided as context to Sign-In -resolvers: +There are also other options, like the this one that looks up a user +by matching the `google.com/email` annotation of user entities in the catalog: ```ts -import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - ... - providerFactories: { - google: createGoogleProvider({ - signIn: { - resolver: async ({ profile: { email } }, ctx) => { - const [id] = email?.split('@') ?? ''; - const userEntityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - // Fetch from an external system that returns entity claims like: - // ['user:default/breanna.davison', ...] - const ent = await externalSystemClient.getUsernames(email); - - // Resolve group membership from the Backstage catalog - const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [id].concat(ent), - logger: ctx.logger, - }); - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: userEntityRef, ent: fullEnt }, - }); - return { id, token }; - }, - }, - }), - ... +providers.google.create({ + signIn: { + resolver: providers.google.resolvers.emailMatchingUserEntityAnnotation(), + }, +}); ``` -The `resolveCatalogMembership` method will retrieve the referenced entities from -the catalog, if possible, and check for -[memberOf](../features/software-catalog/well-known-relations.md#memberof-and-hasmember) -relations to add additional entity claims. +## Custom Ownership Resolution + +If you want to have more control over the membership resolution and token generation +that happens during sign-in you can replace `ctx.signInWithCatalogUser` with a set +of lower-level calls: + +```ts +import { getDefaultOwnershipRefs } from '@backstage/plugin-auth-backend'; + +// This example only shows the resolver function itself. +async ({ profile: { email } }, ctx) => { + if (!email) { + throw new Error('User profile contained no email'); + } + + // This step calls the catalog to look up a user entity. You could for example + // replace it with a call to a different external system. + const { entity } = await ctx.findCatalogUser({ + annotations: { + 'acme.org/email': email, + }, + }); + + // In this step we extract the ownership references from the user entity using + // the standard logic. It uses a reference to the entity itself, as well as the + // target of each `memberOf` relation where the target is of the kind `Group`. + // + // If you replace the catalog lookup with something does not return + // an entity you will need to replace this step as well. + // + // You might also replace it if you for example want to filter out certain groups. + const ownershipRefs = getDefaultOwnershipRefs(entity); + + // The last step is to issue the token, where we might provide more options in the future. + return ctx.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipRefs, + }, + }); +}; +``` + +## Sign-In without Users in the Catalog + +While populating the catalog with organizational data unlocks more powerful ways +to browse your software ecosystem, it might not always be a viable or prioritized +option. However, even if you do not have user entities populated in your catalog, you +can still sign in users. As there are currently no built-in sign-in resolvers for +this scenario you will need to implement your own. + +Signing in a user that doesn't exist in the catalog is as simple as skipping the +catalog lookup step from the above example. Rather than looking up the user, we +instead immediately issue a token using whatever information is available. One caveat +is that it can be tricky to determine the ownership references, although it can +be achieved for example through a lookup to an external service. You typically +want to at least use the user itself as a lone ownership reference. + +```ts +import { DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; + +// This example only shows the resolver function itself. +async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + // We again use the local part of the email as the user name. + const [localPart] = profile.email.split('@'); + + // By using `stringifyEntityRef` we ensure that the reference is formatted correctly + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: localPart, + namespace: DEFAULT_NAMESPACE, + }); + + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); +}, +``` ## AuthHandler diff --git a/docs/auth/index.md b/docs/auth/index.md index 6bda35232a..3beae72d0b 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -1,18 +1,16 @@ --- id: index -title: Adding Authentication -description: How to add authentication to a Backstage application +title: Authentication in Backstage +description: Introduction to authentication in Backstage --- -Authentication in Backstage identifies the user, and provides a way for plugins -to make requests on behalf of a user to third-party services. Backstage can have -zero (guest access), one, or many authentication providers. The default -`@backstage/create-app` template uses guest access for easy startup. +The authentication system in Backstage serves two distinct purposes: sign-in and +identification of users, as well as delegating access to third-party resources. It is possible to +configure Backstage to have any number of authentication providers, but only +one of these will typically be used for sign-in, with the rest being used to provide +access external resources. -See [Using authentication and identity](using-auth.md) for tips on using -Backstage identity information in your app or plugins. - -## Adding an authentication provider +## Built-in Authentication Providers Backstage comes with many common authentication providers in the core library: @@ -31,7 +29,7 @@ These built-in providers handle the authentication flow for a particular service including required scopes, callbacks, etc. These providers are each added to a Backstage app in a similar way. -### Adding provider configuration +## Configuring Authentication Providers Each built-in provider has a configuration block under the `auth` section of `app-config.yaml`. For example, the GitHub provider: @@ -56,25 +54,37 @@ allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. -## Using an authentication provider for sign-in +## Sign-In Configuration -If you want to use an authentication provider for sign-in, as opposed to just accessing external resources, you'll need to configure that in your app as well. This is done by providing a custom `SignInPage` component to the app, which will require the user to sign in before they can access the app. Note that this does not block access to the app, which you can read more about [here](./using-auth.md). +> NOTE: Identity management and the `SignInPage` in Backstage is NOT a method +> for blocking access for unauthorized users, that either requires additional +> backend implementation or a separate service like Google's Identity-Aware +> Proxy. The identity system only serves to provide a personalized experience +> and access to a Backstage Identity Token, which can be passed to backend +> plugins. -If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. These reference the `ApiRef` exported for the provider. +Using an authentication provide for sign-in is something you need to configure +both in the frontend app, as well as the `auth` backend plugin. For information +on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). +The rest of this section will focus on how to configure sign-in for the frontend app. -Again, the following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: +Sign-in is configured by providing a custom `SignInPage` app component. It will +rendered before any other routes in the app and is responsible for providing the +identity of the current user. The `SignInPage` can render any number of pages and +components, or just blank space with logic running in the background. In the end +however it must provide a valid Backstage user identity through the `onSignInSuccess` +callback prop, at which point the rest of the app is rendered. + +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, +which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. + +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, +and can be adapted to any of the built-in providers: ```diff + import { githubAuthApiRef } from '@backstage/core-plugin-api'; -+ import { SignInProviderConfig, SignInPage } from '@backstage/core-components'; ++ import { SignInPage } from '@backstage/core-components'; -+ const githubProvider: SignInProviderConfig = { -+ id: 'github-auth-provider', -+ title: 'GitHub', -+ message: 'Sign in using GitHub', -+ apiRef: githubAuthApiRef, -+}; -+ const app = createApp({ apis, + components: { @@ -82,15 +92,20 @@ Again, the following example for GitHub shows the additions needed to `packages/ + + ), + }, bindRoutes({ bind }) { ``` -To also allow unauthenticated guest access, use the `providers` prop for -`SignInPage`: +You can also use the `providers` prop to enable multiple sign-in methods, for example +allows allowing guest access: ```diff const app = createApp({ @@ -99,14 +114,60 @@ To also allow unauthenticated guest access, use the `providers` prop for + SignInPage: props => ( + + ), + }, bindRoutes({ bind }) { ``` -## Adding a custom authentication provider +## For Plugin Developers + +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers +to use, both to access the user identity, as well as third party resources. + +### Identity for Plugin Developers + +For plugin developers, there is one main touchpoint for accessing the user identity: the +`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. + +The `IdentityApi` gives access to the signed-in user's identity in the frontend. +It provides access to the user's entity reference, lightweight profile information, and +a Backstage token that identifies the user when making authenticated calls within Backstage. + +When making calls to backend plugins, we recommend that the `FetchApi` is used, which +is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will +automatically include a Backstage token in the request, meaning there is no need +to interact directly with the `IdentityApi`. + +### Accessing Third Party Resources + +A common pattern for talking to third party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls we keep the coupling between the frontend and +backend low, and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to for example a session-based system, +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third party services is +through [Utility APIs](../api/utility-apis.md) for each service provider. These +are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a +full list of providers, see the +[@backstage/core-plugin-api](../reference/core-plugin-api.md#variables) reference. + +## Custom Authentication Provider There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that diff --git a/docs/auth/using-auth.md b/docs/auth/using-auth.md deleted file mode 100644 index 067c43e8a0..0000000000 --- a/docs/auth/using-auth.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -id: using-auth -title: Using authentication and identity -description: How to use authentication and identity in Backstage ---- - -The Auth APIs in Backstage identify the user, and provide a way for plugins to -request access to 3rd party services on behalf of the user (OAuth). This -documentation focuses on the implementation of that solution and how to extend -it. - -### Accessing Third Party Services - -The main pattern for talking to third party services in Backstage is -user-to-server requests, where short-lived OAuth Access Tokens are requested by -plugins to authenticate calls to external services. These calls can be made -either directly to the services or through a backend plugin or service. - -By relying on user-to-server calls we keep the coupling between the frontend and -backend low, and provide a much lower barrier for plugins to make use of third -party services. This is in comparison to for example a session-based system, -where access tokens are stored server-side. Such a solution would require a much -deeper coupling between the auth backend plugin, its session storage, and other -backend plugins or separate services. A goal of Backstage is to make it as easy -as possible to create new plugins, and an auth solution based on user-to-server -OAuth helps in that regard. - -The method with which frontend plugins request access to third party services is -through [Utility APIs](../api/utility-apis.md) for each service provider. For a -full list of providers, see the -[@backstage/core-plugin-api](../reference/core-plugin-api.md#variables) -reference. - -### Identity - WIP - -> NOTE: Identity management and the `SignInPage` in Backstage is NOT a method -> for blocking access for unauthorized users, that either requires additional -> backend implementation or a separate service like Google's Identity-Aware -> Proxy. The identity system only serves to provide a personalized experience -> and access to a Backstage Identity Token, which can be passed to backend -> plugins. - -Identity management is still work in progress, but there are already a couple of -pieces in place that can be used. - -#### Identity for Plugin Developers - -As a plugin developer, there are two main touchpoints for identities: the -`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`, -and a not yet existing middleware exported by `@backstage/backend-common`. - -The `IdentityApi` gives access to the signed-in user's identity in the frontend. -It provides access to the user's ID, lightweight profile information, and an ID -token used to make authenticated calls within Backstage. - -The middleware that will be provided by `@backstage/backend-common` allows -verification of Backstage ID tokens, and optionally loading additional -information about the user. The progress is tracked in -https://github.com/backstage/backstage/issues/1435. - -#### Identity for App Developers - -If you're setting up your own Backstage app, or want to add a new identity -provider, there are three touchpoints: the frontend auth APIs in -`@backstage/core-app-api` and `@backstage/core-plugin-api`, the backend auth -providers in `auth-backend`, and the `SignInPage` component configured in the -Backstage app via `createApp`. - -The frontend APIs and backend providers are tightly coupled together for each -auth provider, and together they implement an e2e auth flow. Only some auth -providers also act as identity providers though. For example, at the moment of -writing, the Google Auth provider is able to act as a Backstage identity -provider, but the GitHub one can not. For an auth provider to also act as an -identity provider, it needs to implement the `BackstageIdentityApi` in the -frontend, and in the backend it needs to return a `BackstageIdentity` structure. - -It is up to each provider to implement the mapping between a provider identity -and the corresponding Backstage identity. That is currently still work in -progress, and as a stop-gap for example the Google provider returns the local -part of the user's email as the user ID. - -The final piece of the puzzle is the `SignInPage` component that can be -configured as part of the app. Without a sign-in page, Backstage will fall back -to a `guest` identity for all users, without any ID token. To enable sign-in, a -`SignInPage` needs to be configured, which in turn has to supply a user to the -app. The `@backstage/core-components` package provides a basic sign-in page that -allows both the user and the app developer to choose between a couple of -different sign-in methods, or to designate a single provider that may also be -logged in to automatically. - -## Further Reading - -More details are provided in dedicated sections of the documentation. - -- [OAuth](./oauth.md): Description of the generic OAuth flow implemented by the - [auth-backend](https://github.com/backstage/backstage/tree/master/plugins/auth-backend). -- [Glossary](./glossary.md): Glossary of some common terms related to the auth - flows. diff --git a/docs/conf/writing.md b/docs/conf/writing.md index a4da1e3df0..aef9620373 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -161,7 +161,7 @@ $file: ./my-secret.txt The `$include` keyword can be used to load configuration values from an external file. It's able to load and parse data from `.json`, `.yml`, and `.yaml` files. -It's also possible to include a url fragment (`#`) to point to a value at the +It's also possible to include a URL fragment (`#`) to point to a value at the given path in the file, using a dot-separated list of keys. For example, the following would read `my-secret-key` from `my-secrets.json`: diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 0b99e81d64..01bc6279f2 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -59,18 +59,19 @@ Once the host build is complete, we are ready to build our image. The following FROM node:16-bullseye-slim WORKDIR /app + +# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. # The skeleton contains the package.json of each package in the monorepo, # and along with yarn.lock and the root package.json, that's enough to run yarn install. COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -# install sqlite3 dependencies -RUN apt-get update && \ - apt-get install -y libsqlite3-dev python3 cmake g++ && \ - rm -rf /var/lib/apt/lists/* && \ - yarn config set python /usr/bin/python3 - RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. @@ -159,7 +160,8 @@ WORKDIR /app COPY --from=packages /app . # install sqlite3 dependencies -RUN apt-get update && apt-get install -y libsqlite3-dev python3 cmake g++ && \ +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ yarn config set python /usr/bin/python3 RUN yarn install --frozen-lockfile --network-timeout 600000 && rm -rf "$(yarn cache dir)" @@ -176,16 +178,16 @@ FROM node:16-bullseye-slim WORKDIR /app +# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + # Copy the install dependencies from the build stage and context COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -# install sqlite3 dependencies -RUN apt-get update && \ - apt-get install -y libsqlite3-dev python3 cmake g++ && \ - rm -rf /var/lib/apt/lists/* && \ - yarn config set python /usr/bin/python3 - RUN yarn install --frozen-lockfile --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" # Copy the built packages from the build stage diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index dd7e666be4..cb30451c7d 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -228,7 +228,13 @@ For example: ```yaml - type: 'gke' projectId: 'gke-clusters' - region: 'europe-west1' + region: 'europe-west1' # optional + skipTLSVerify: false # optional + skipMetricsLookup: false # optional + exposeDashboard: false # optional + matchingResourceLabels: # optional + - key: 'environment' + value: 'production' ``` Will configure the Kubernetes plugin to connect to all GKE clusters in the @@ -247,17 +253,17 @@ The Google Cloud project to look for Kubernetes clusters in. The Google Cloud region to look for Kubernetes clusters in. Defaults to all regions. -##### `skipTLSVerify` +##### `skipTLSVerify` (optional) This determines whether the Kubernetes client verifies the TLS certificate presented by the API server. Defaults to `false`. -##### `skipMetricsLookup` +##### `skipMetricsLookup` (optional) This determines whether the Kubernetes client looks up resource metrics CPU/Memory for pods returned by the API server. Defaults to `false`. -##### `exposeDashboard` +##### `exposeDashboard` (optional) This determines whether the `dashboardApp` and `dashboardParameters` should be automatically configured in order to expose the GKE dashboard from the @@ -265,6 +271,11 @@ Kubernetes plugin. Defaults to `false`. +##### `matchingResourceLabels` (optional) + +Array of key value labels used to filter out clusters which don't have the matching +[resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels). + #### Custom `KubernetesClustersSupplier` If the configuration-based cluster locators do not work for your use-case, @@ -355,6 +366,16 @@ annotations: 'backstage.io/kubernetes-id': dice-roller ``` +#### Adding the namespace annotation + +Entities can have the `backstage.io/kubernetes-namespace` annotation, this will cause the entity's Kubernetes resources +to by looked up via that namespace. + +```yaml +annotations: + 'backstage.io/kubernetes-namespace': dice-space +``` + #### Labeling Kubernetes components In order for Kubernetes components to show up in the software catalog as a part diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index e920bd44ea..ec0355c7d8 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -13,9 +13,7 @@ If you haven't setup Backstage already, read the ## Adding the Kubernetes frontend plugin The first step is to add the Kubernetes frontend plugin to your Backstage -application. Navigate to your new Backstage application directory, and then to -your `packages/app` directory, and install the `@backstage/plugin-kubernetes` -package. +application. ```bash # From your Backstage root directory diff --git a/docs/features/search/README.md b/docs/features/search/README.md index ed950d8ac8..a151da1736 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -24,15 +24,12 @@ Backstage ecosystem. ## Project roadmap -| Version | Description | -| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | -| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | -| Backstage Search Beta ✅ | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | -| [Backstage Search GA ⌛][ga] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See GA Use Cases](#backstage-search-ga). | - -[beta]: https://github.com/backstage/backstage/milestone/27 -[ga]: https://github.com/backstage/backstage/milestone/28 +| Version | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | +| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | +| Backstage Search Beta ✅ | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | +| [Backstage Search 1.0 ⌛] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See 1.0 Use Cases](#backstage-search-1.0). | ## Use Cases @@ -82,9 +79,9 @@ are met, and can be deployed using a production-ready search engine. search engine according to my organization's needs, but a sensible default query should be in place so that I am not required to do so. -#### Backstage Search GA +#### Backstage Search 1.0 -We will consider Backstage Search to be generally available (GA) when the above +We will consider Backstage Search to be 1.0 when the above use-cases are met, and an ecosystem of search-enabled plugins are available and stable. @@ -113,22 +110,26 @@ search engines. ## Plugins Integrated with Search -| Plugin | Support Status | -| -------------------------------------------------------------- | -------------- | -| Catalog | ✅ | -| [TechDocs](./how-to-guides.md#how-to-index-techdocs-documents) | ✅ | +| Plugin | Support Status | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | +| Catalog | ✅ | +| [TechDocs](./how-to-guides.md#how-to-index-techdocs-documents) | ✅ | +| [Stack Overflow](https://github.com/backstage/backstage/blob/master/plugins/stack-overflow-backend/README.md#index-stack-overflow-questions-to-search) | ✅ | [Reach out to us](#get-involved) if you want to chat about support for more plugins integrated to search. ## Tech Stack -| Stack | Location | -| --------------- | ------------------------------------- | -| Frontend Plugin | @backstage/plugin-search | -| Backend Plugin | @backstage/plugin-search-backend | -| Indexer Plugin | @backstage/plugin-search-backend-node | -| Common Code | @backstage/plugin-search-common | +| Stack | Location | +| ------------------------- | ----------------------------------------------------- | +| Frontend Plugin | @backstage/plugin-search | +| Frontend Plugin Library | @backstage/plugin-search-react | +| Isomorphic Plugin Library | @backstage/plugin-search-common | +| Backend Plugin | @backstage/plugin-search-backend | +| Backend Plugin Library | @backstage/plugin-search-backend-node | +| Backend Plugin Module | @backstage/plugin-search-backend-module-elasticsearch | +| Backend Plugin Module | @backstage/plugin-search-backend-module-pg | ## Get Involved diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 3ff08e6e73..8618e04781 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -74,17 +74,17 @@ export const myCustomValidation = ( This is where the magic happens and creates the custom field extension. Note that if you're writing extensions part of a separate plugin, - then please use `plugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`. + then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`. */ import { - plugin, + scaffolderPlugin, createScaffolderFieldExtension, } from '@backstage/plugin-scaffolder'; import { MyCustomExtension } from './MyCustomExtension'; import { myCustomValidation } from './validation'; -export const MyCustomFieldExtension = plugin.provide( +export const MyCustomFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ name: 'MyCustomExtension', component: MyCustomExtension, diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 340c0d9396..c64d0b785d 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -76,30 +76,34 @@ TechDocs packages: TechDocs promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). +**v1.2** 🚧 + +With the Backstage 1.2 release, we plan to introduce the [TechDocs Addon Framework](https://github.com/backstage/backstage/issues/9636) for augmenting the TechDocs experience at read-time. + +In addition to the framework itself, we'll be open sourcing a `` addon, helping you to create a feedback loop that drives up documentation quality and fosters a documentation culture at your organization. + ### **Future work 🔮** Some of the following items are coming soon and some are potential ideas. -- [TechDocs Addon Framework](https://github.com/backstage/backstage/issues/9636) -- Contribute to and deploy from a marketplace of TechDocs Addons -- Addon: Highlight text and raise an Issue to create a feedback loop to drive up documentation quality -- Addon: MDX (allows you to use JSX in your Markdown content) - Better integration with [Scaffolder V2](https://github.com/backstage/backstage/issues/2771) (e.g. easy to choose and plug documentation template with Software Templates) -- Static site generator agnostic +- Static site generator agnostic, including possible support for MDX (allowing you to use JSX in your Markdown content) - Possible to configure several aspects about TechDocs (e.g. URL, homepage, theme) ## Tech stack -| Stack | Location | -| ----------------------------------------------- | -------------------------------------------------------- | -| Frontend Plugin | [`@backstage/plugin-techdocs`][techdocs/frontend] | -| Backend Plugin | [`@backstage/plugin-techdocs-backend`][techdocs/backend] | -| CLI (for local development and generating docs) | [`@techdocs/cli`][techdocs/cli] | -| Docker Container (for generating docs) | [`techdocs-container`][techdocs/container] | +| Stack | Location | +| ----------------------------------------------- | --------------------------------------------------------------- | +| Frontend Plugin | [`@backstage/plugin-techdocs`][techdocs/frontend] | +| Frontend Plugin Library | [`@backstage/plugin-techdocs-react`][techdocs/frontend-library] | +| Backend Plugin | [`@backstage/plugin-techdocs-backend`][techdocs/backend] | +| CLI (for local development and generating docs) | [`@techdocs/cli`][techdocs/cli] | +| Docker Container (for generating docs) | [`techdocs-container`][techdocs/container] | [techdocs/frontend]: https://github.com/backstage/backstage/blob/master/plugins/techdocs +[techdocs/frontend-library]: https://github.com/backstage/backstage/blob/master/plugins/techdocs-react [techdocs/backend]: https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend [techdocs/container]: https://github.com/backstage/techdocs-container [techdocs/cli]: https://github.com/backstage/techdocs-cli diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md new file mode 100644 index 0000000000..0fba3b92e3 --- /dev/null +++ b/docs/features/techdocs/addons.md @@ -0,0 +1,218 @@ +--- +id: addons +title: TechDocs Addons +description: How to find, use, or create TechDocs Addons. +--- + +> Note: This page contains documentation for features that have not yet been +> released on a [main-line version](https://backstage.io/docs/overview/versioning-policy#main-release-line) +> of Backstage and TechDocs. We plan to make Addons generally available in +> Backstage v1.2. + +## Concepts + +TechDocs is a centralized platform for publishing, viewing, and discovering +technical documentation across an entire organization. It's a solid foundation! +But it doesn't solve higher-order documentation needs on its own: how do you +create and reinforce a culture of documentation? How do you build trust in the +quality of technical documentation? + +TechDocs Addons are a mechanism by which you can customize the TechDocs +experience in order to try and address some of these higher-order needs. + +### Addons + +An Addon is just a react component. Like any react component, it can retrieve +and render data using normal Backstage or native hooks, APIs, and components. +Props can be used to configure its behavior, where appropriate. + +### Locations + +Addons declare a `location` where they will be rendered. Most locations are +representative of physical spaces in the TechDocs UI: + +- `Header`: For Addons which fill up the header from the right, on the same + line as the title. +- `Subheader`: For Addons that sit below the header but above all content. + This is a great location for tooling/configuration of TechDocs display. +- `PrimarySidebar`: Left of the content, above of the navigation. +- `SecondarySidebar`: Right of the content, above the table of contents. +- `Content`: A special location intended for Addons which augment the + statically generated content of the documentation itself. +- `Component`: A [proposed-but-not-yet-implemented](https://github.com/backstage/backstage/issues/11109) + virtual location, aimed at simplifying a common type of Addon. + +TechDocs Addon Location Guide + +### Addon Registry + +The installation and configuration of Addons happens within a Backstage app's +frontend. Addons are imported from plugins and added underneath a registry +component called ``. This registry can be configured for both +the TechDocs Reader page as well as the Entity docs page. + +Addons are rendered in the order in which they are registered. + +## Installing and using Addons + +Addons can be installed and configured in much the same way as extensions for +other Backstage plugins: by adding them underneath an extension registry +component (``) under the route representing the TechDocs Reader +page in your `App.tsx`: + +```tsx +// packages/app/src/App.tsx + +import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +// ... + +}> + + + {/* Other addons can be added here. */} + +; +``` + +The process for configuring Addons on the documentation tab on the entity page +is very similar; instead of adding the `` registry under a +``, you'd add it as a child of ``: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityLayout } from '@backstage/plugin-catalog'; +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +// ... + + + + + + {/* Other addons can be added here. */} + + +; +``` + +Note that on the entity page, because the Catalog plugin is responsible for the +page header, TechDocs Addons whose location is `Header` will not be rendered. + +## Available Addons + +Addons can, in principle, be provided by any plugin! To make it easier to +discover available Addons, we've compiled a list of them here: + +| Addon | Package/Plugin | Description | +| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | + +Got an Addon to contribute? Feel free to add a row above! + +## Creating an Addon + +The simplest Addons are plain old react components that get rendered in +specific locations within a TechDocs site. To package such a react component as +an Addon, follow these steps: + +1. Write the component in your plugin like any other component +2. Create, provide, and export the component from your plugin + +```ts +// plugins/your-plugin/src/plugin.ts + +import { + createTechDocsAddonExtension, + TechDocsAddonLocations, +} from '@backstage/plugin-techdocs-react/alpha'; +import { CatGifComponent, CatGifComponentProps } from './addons'; + +// ... + +// You must "provide" your Addon, just like any extension, via your plugin. +export const CatGif = yourPlugin.provide( + // This function "creates" the Addon given a component and location. If your + // component can be configured via props, pass the prop type here too. + createTechDocsAddonExtension({ + name: 'CatGif', + location: TechDocsAddonLocations.Header, + component: CatGifComponent, + }), +); +``` + +### Addons in the Content location + +Beyond the "render a component in a region" use-case, it's also possible for +Addons to access and manipulate a TechDocs site's DOM; this could be used to, +for example, load and instantiate client-side diagramming libraries, replace +elements with dynamically loaded content, etc. + +This type of Addon is still expressed as a react component, but instead of +returning a react element to be rendered, it updates the DOM via side-effects +(e.g. with `useEffect`). Access to the DOM is made available via utility hooks +provided by the Addon framework. + +```tsx +// plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx + +import React, { useEffect } from 'react'; +import { useShadowRootElements } from '@backstage/plugin-techdocs-react/alpha'; + +// This is a normal react component; in order to make it an Addon, you would +// still create and provide it via your plugin as described above. The only +// difference is that you'd set `location` to `TechDocsAddonLocations.Content`. +export const MakeAllImagesCatGifsAddon = () => { + // This hook can be used to get references to specific elements. If you need + // access to the whole shadow DOM, use the the underlying useShadowRoot() + // hook instead. + const images = useShadowRootElements(['img']); + + useEffect(() => { + images.forEach(img => { + if (img.src !== 'https://example.com/cat.gif') { + img.src = 'https://example.com/cat.gif'; + } + }); + }, [images]); + + // Nothing to render directly, so we can just return null. + return null; +}; +``` + +### Testing Addons + +Install `@backstage/plugin-techdocs-addons-test-utils` as a `devDependency` in +your plugin for access to utilities that make testing such Addons easier. + +A test for the above Addon might look something like this: + +```tsx +// plugins/your-plugin/src/addons/MakeAllImagesCatGifs.test.tsx +import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; + +// Note: import your actual addon (the one provided by your plugin). +import { MakeAllImagesCatGifs } from '../plugin.ts'; + +describe('MakeAllImagesCatGifs', () => { + it('replaces img srcs with cat gif', async () => { + const { getByTestId } = await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom() + .renderWithEffects(); + + expect(getByTestId('fixture')).toHaveAttribute( + 'src', + 'https://example.com/cat.gif', + ); + }); +}); +``` diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 6f6dbf7552..9a7dccd5a6 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -120,7 +120,7 @@ Generate TechDocs documentation site using MkDocs. Options: --source-dir Source directory containing mkdocs.yml and docs/ directory. (default: ".") --output-dir Output directory containing generated TechDocs site. (default: "./site/") - --docker-image The mkdocs docker container to use (default: "spotify/techdocs:v0.3.6") + --docker-image The mkdocs docker container to use (default: "spotify/techdocs:v1.0.2") --no-pull Do not pull the latest docker image --no-docker Do not use Docker, use MkDocs executable and plugins in current user environment. --techdocs-ref The repository hosting documentation source files e.g. diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index e34f58bc70..2944197dfb 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -18,8 +18,8 @@ GitLab, etc.) and passes the files to the generator for next steps. There are two kinds of preparers available - -1. Common Git Preparer - Uses `git clone` on any repository url. -2. Url Reader - Uses source code hosting provider's API to download files. +1. Common Git Preparer - Uses `git clone` on any repository URL. +2. URL Reader - Uses source code hosting provider's API to download files. (Faster and recommended) ### TechDocs Generator @@ -96,10 +96,6 @@ Documentation generated by TechDocs is generated as static HTML sites. The TechDocs Reader was therefore created to be able to integrate pre-generated HTML sites with the Backstage UI. -The TechDocs Reader purpose is also to open up the opportunity to integrate -TechDocs widgets for a customized full-featured TechDocs experience. -([coming soon V.3](./README.md#project-roadmap)) - [TechDocs Reader](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/reader/README.md) ## Transformers @@ -110,3 +106,19 @@ transform the HTML content on pre and post render (e.g. rewrite docs links or modify css). [Transformers API docs](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/reader/README.md) + +## TechDocs Addons + +Addons (introduced in Backstage v1.2) are client-side, React-based extensions +that can be used to augment the TechDocs experience at read-time. They offer a +mechanism for configuring the TechDocs Reader to better suit your +organization's needs. + +Addons can dynamically load and display information anywhere in the TechDocs +Reader, including within the statically generated content itself. + +Addons should not be confused with `mkdocs` plugins, which may be used to +customize a TechDocs site's content at build-time. While it's possible to take +advantage of some `mkdocs` plugins, not all such plugins play well with +TechDocs (primarily, but not exclusively, for security reasons). Addons offer +an alternative. diff --git a/docs/features/techdocs/configuring-ci-cd.md b/docs/features/techdocs/configuring-ci-cd.md index a4da4e5d95..0f8044e9c9 100644 --- a/docs/features/techdocs/configuring-ci-cd.md +++ b/docs/features/techdocs/configuring-ci-cd.md @@ -29,7 +29,7 @@ cd repo # Install @techdocs/cli, mkdocs and mkdocs plugins npm install -g @techdocs/cli -pip install mkdocs-techdocs-core==0.* +pip install mkdocs-techdocs-core==1.* # Generate techdocs-cli generate --no-docker @@ -167,7 +167,7 @@ jobs: run: sudo npm install -g @techdocs/cli - name: Install mkdocs and mkdocs plugins - run: python -m pip install mkdocs-techdocs-core==0.* + run: python -m pip install mkdocs-techdocs-core==1.* - name: Generate docs site run: techdocs-cli generate --no-docker --verbose diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 30d846efda..1eeebae903 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -227,7 +227,7 @@ You can do so by including the following lines in the last step of your ```Dockerfile RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install mkdocs-techdocs-core==0.2.2 +RUN pip3 install mkdocs-techdocs-core==1.0.1 ``` Please be aware that the version requirement could change, you need to check our diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 56ca2252d0..4f2dc6c348 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -162,20 +162,11 @@ The `techDocsPage` is a default techdocs reader page which lives in having to set anything up. ```tsx - - {({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => ( - <> - - - - - - )} - + + + + + ``` If you would like to compose your own `techDocsPage`, you can do so by replacing @@ -183,42 +174,30 @@ the children of TechDocsPage with something else. Maybe you are _just_ interested in replacing the Header: ```tsx - - {({ entityRef, onReady }) => ( - <> -
- - - - - )} - + +
+ + ``` Or maybe you want to disable the in-context search ```tsx - - {({ entityRef, onReady }) => ( - <> -
- - - - - )} - + +
+ + ``` Or maybe you want to replace the entire TechDocs Page. ```tsx - +

my own content

- + ``` ## How to migrate from TechDocs Alpha to Beta @@ -458,7 +437,7 @@ FROM python:3.8-alpine RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig -RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.2.1 +RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==1.0.1 RUN pip install mkdocs-kroki-plugin diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 66eaf00663..5a53961e81 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -461,7 +461,7 @@ Since the new SDK doesn't use the old way authentication, we don't need the keys The new SDK needs the OpenStack Swift connection URL for connecting the Swift. So you need to add a new key called `openStackSwift.swiftUrl` and give the -OpenStack Swift url here. Example url should look like that: +OpenStack Swift URL here. Example URL should look like that: `https://example.com:6780/swift/v1` ##### That's it! diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 95fb2afc12..fc78217fbc 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -193,9 +193,7 @@ go to your Backstage portal in your browser, you should have your login prompt! To learn more about Authentication in Backstage, here are some docs you could read: -- [Adding Authentication](../auth/) -- [Adding a new Authentication Provider](../auth/add-auth-provider.md) -- [Using authentication and identity](../auth/using-auth.md) +- [Authentication in Backstage](../auth/) - [Using organizational data from GitHub](../integrations/github/org.md) ### Setting up a GitHub Integration @@ -203,7 +201,7 @@ could read: The GitHub integration supports loading catalog entities from GitHub or GitHub Enterprise. Entities can be added to static catalog configuration, registered with the catalog-import plugin, or discovered from a GitHub organization. Users -and Groups can also be loaded from an organization. While using GitHub Apps +and Groups can also be loaded from an organization. While using [GitHub Apps](../integrations/github/github-apps.md) might be the best way to set up integrations, for this tutorial you'll use a Personal Access Token. @@ -217,25 +215,35 @@ for 7 days, it's a lucky number. Screenshot of the GitHub Personal Access Token creation page

-Set the scope to your likings. For this tutorial, selecting "repo" and "workflow" is required as the scaffolding job in this guide configures a GitHub actions workflow for the newly created project. -enough. +Set the scope to your likings. For this tutorial, selecting `repo` and `workflow` is required as the scaffolding job in this guide configures a GitHub actions workflow for the newly created project. -In the `app-config.yaml`, search for `integrations:` and add your token, like we -did in below example: +For this tutorial, we will be writing the token to `app-config.local.yaml`. This file might not exist for you, so if it doesn't go ahead and create it alongside the `app-config.yaml` at the root of the project. +This file should also be excluded in `.gitignore`, to avoid accidental committing of this file. + +In your `app-config.local.yaml` go ahead and add the following: ```yaml integrations: github: - host: github.com - token: ghp_urtokendeinfewinfiwebfweb + token: ghp_urtokendeinfewinfiwebfweb # this should be the token from GitHub ``` That's settled. This information will be leveraged by other plugins. +If you're looking for a more production way to manage this secret, then you can do the following with the token being stored in an environment variable called `GITHUB_TOKEN`. + +```yaml +integrations: + github: + - host: github.com + token: ${GITHUB_TOKEN} # this will use the environment variable GITHUB_TOKEN +``` + Some helpful links, for if you want to learn more about: - [Other available integrations](../integrations/) -- [Using GitHub Apps instead of a Personal Access Token](../plugins/github-apps.md#docsNav) +- [Using GitHub Apps instead of a Personal Access Token](../integrations/github/github-apps.md#docsNav) ### Explore what we've done so far diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index b67e847272..cbe84db2e0 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -24,7 +24,7 @@ the code. - [`.github/`](https://github.com/backstage/backstage/tree/master/.github) - Standard GitHub folder. It contains - amongst other things - our workflow definitions and templates. Worth noting is the - [styles](https://github.com/backstage/backstage/tree/master/.github/styles) + [vale](https://github.com/backstage/backstage/tree/master/.github/vale) sub-folder which is used for a markdown spellchecker. - [`.yarn/`](https://github.com/backstage/backstage/tree/master/.yarn) - @@ -212,9 +212,6 @@ future. common for companies to have their own npm registry, and this file makes sure that this folder always uses the public registry. -- [`.vale.ini`](https://github.com/backstage/backstage/tree/master/.vale.ini) - - [Spell checker](https://github.com/errata-ai/vale) for Markdown files. - - [`.yarnrc`](https://github.com/backstage/backstage/tree/master/.yarnrc) - Enforces "our" version of Yarn. diff --git a/docs/integrations/bitbucket/locations.md b/docs/integrations/bitbucket/locations.md index 67c4dcac33..c0c6afc7a3 100644 --- a/docs/integrations/bitbucket/locations.md +++ b/docs/integrations/bitbucket/locations.md @@ -6,38 +6,51 @@ sidebar_label: Locations description: Integrating source code stored in Bitbucket into the Backstage catalog --- -The Bitbucket integration supports loading catalog entities from bitbucket.org -or a self-hosted Bitbucket. Entities can be added to +The Bitbucket integration supports loading catalog entities from bitbucket.org (Bitbucket Cloud) +or Bitbucket Server. Entities can be added to [static catalog configuration](../../features/software-catalog/configuration.md), or registered with the [catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import) plugin. +## Bitbucket Cloud + ```yaml integrations: - bitbucket: - - host: bitbucket.org - token: ${BITBUCKET_TOKEN} + bitbucketCloud: + - username: ${BITBUCKET_CLOUD_USERNAME} + appPassword: ${BITBUCKET_CLOUD_PASSWORD} ``` -> Note: A public Bitbucket provider is added automatically at startup for -> convenience, so you only need to list it if you want to supply a -> [token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). +> Note: A public Bitbucket Cloud provider is added automatically at startup for +> convenience, so you only need to list it if you want to supply credentials. -Directly under the `bitbucket` key is a list of provider configurations, where -you can list the Bitbucket providers you want to fetch data from. Each entry is -a structure with up to four elements: +Directly under the `bitbucketCloud` key is a list of provider configurations, where +you can list the Bitbucket Cloud providers you want to fetch data from. +In the case of Bitbucket Cloud, you will have up to one entry. -- `host`: The host of the Bitbucket instance, e.g. `bitbucket.company.com`. -- `token` (optional): An personal access token as expected by Bitbucket. Either - an access token **or** a username + appPassword may be supplied. -- `username` (optional): The Bitbucket username to use in API requests. If +This one entry will have the following elements: + +- `username`: The Bitbucket Cloud username to use in API requests. If neither a username nor token are supplied, anonymous access will be used. -- `appPassword` (optional): The password for the Bitbucket user. Only needed - when using `username` instead of `token`. -- `apiBaseUrl` (optional): The URL of the Bitbucket API. For self-hosted - installations, it is commonly at `https:///rest/api/1.0`. For - bitbucket.org, this configuration is not needed as it can be inferred. +- `appPassword`: The app password for the Bitbucket Cloud user. -> Note: If you are using Bitbucket server you MUST set the username as well as -> the token or appPassword. +## Bitbucket Server + +```yaml +integrations: + bitbucketServer: + - host: bitbucket.company.com + token: ${BITBUCKET_SERVER_TOKEN} +``` + +Directly under the `bitbucketServer` key is a list of provider configurations, where +you can list the Bitbucket Server providers you want to fetch data from. Each entry is +a structure with the following elements: + +- `host`: The host of the Bitbucket Server instance, e.g. `bitbucket.company.com`. +- `token` (optional): + An [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) + as expected by Bitbucket Server. +- `apiBaseUrl` (optional): The URL of the Bitbucket Server API. For self-hosted + installations, it is commonly at `https:///rest/api/1.0`. diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md new file mode 100644 index 0000000000..5cd1f6204b --- /dev/null +++ b/docs/integrations/gerrit/discovery.md @@ -0,0 +1,69 @@ +--- +id: discovery +title: Gerrit Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from Gerrit repositories +--- + +The Gerrit integration has a special entity provider for discovering catalog entities +from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get +a list of repositories and will automatically ingest all `catalog-info.yaml` files +stored in the root of the matching projects. + +## Installation + +As this provider is not one of the default providers, you will first need to install +the Gerrit provider plugin: + +```bash +# From the Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gerrit +``` + +Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: + +```ts +/* packages/backend/src/plugins/catalog.ts */ +import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit'; +import { Duration } from 'luxon'; +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + ...GerritEntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), +); +``` + +## Configuration + +To use the discovery processor, you'll need a Gerrit integration +[set up](locations.md). Then you can add any number of providers. + +```yaml +# app-config.yaml +catalog: + providers: + gerrit: + yourProviderId: # identifies your dataset / provider independent of config changes + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=webapps' + backend: + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=backend' +``` + +The provider configuration is composed of three parts: + +- host, the host of the Gerrit integration to use. +- branch, the branch where we will look for catalog entities (defaults to "master"). +- query, this string is directly used as the argument to the "List Project" API. + Typically you will want to have some filter here to exclude projects that will + never contain any catalog files. diff --git a/docs/integrations/gerrit/locations.md b/docs/integrations/gerrit/locations.md index 8091de3a2e..9bff2385e3 100644 --- a/docs/integrations/gerrit/locations.md +++ b/docs/integrations/gerrit/locations.md @@ -19,7 +19,8 @@ To use this integration, add at least one Gerrit configuration to your root `app integrations: gerrit: - host: gerrit.company.com - apiBaseUrl: https://gerrit.company.com/gerrit + baseUrl: https://gerrit.company.com/gerrit + cloneUrl: https://gerrit.company.com/clone gitilesBaseUrl: https://gerrit.company.com/gitiles username: ${GERRIT_USERNAME} password: ${GERRIT_PASSWORD} @@ -27,13 +28,15 @@ integrations: Directly under the `gerrit` key is a list of provider configurations, where you can list the Gerrit instances you want to fetch data from. Each entry is -a structure with up to four elements: +a structure with up to six elements: - `host`: The host of the Gerrit instance, e.g. `gerrit.company.com`. -- `apiBaseUrl` (optional): Needed if the Gerrit instance is not reachable at +- `baseUrl` (optional): Needed if the Gerrit instance is not reachable at the base of the `host` option (e.g. `https://gerrit.company.com`) set the address here. This is the address that you would open in a browser. -- `gitilesBaseUrl` (optional): This is needed for creating a valid user-friendly url +- `cloneUrl` (optional): The base URL for HTTP clones. Will default to `baseUrl` if + not set. The address used to clone a repo is the `cloneUrl` plus the repo name. +- `gitilesBaseUrl` (optional): This is needed for creating a valid user-friendly URL that can be used for browsing the content of the provider. If not set a default value will be created in the same way as the "baseUrl" option. There is no requirement to have Gitiles for the Backstage Gerrit integration but without it diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 1cbf8905a2..a0c3fed153 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -60,8 +60,9 @@ And then add the processors to your catalog builder: ## Configuration To use the discovery processor, you'll need a GitHub integration -[set up](locations.md) with a `GITHUB_TOKEN`. Then you can add a location target -to the catalog configuration: +[set up](locations.md) with either a [Personal Access Token](../../getting-started/configuration.md#setting-up-a-github-integration) or [GitHub Apps](./github-apps.md). + +Then you can add a location target to the catalog configuration: ```yaml catalog: @@ -96,14 +97,12 @@ When using a custom pattern, the target is composed of three parts: ## GitHub API Rate Limits -GitHub [rate limits] API requests to 5,000 per hour (or more for Enterprise +GitHub [rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) API requests to 5,000 per hour (or more for Enterprise accounts). The default Backstage catalog backend refreshes data every 100 seconds, which issues an API request for each discovered location. This means if you have more than ~140 catalog entities, you may get throttled by -rate limiting. This will soon be resolved once catalog refreshes make use of -ETags; to work around this in the meantime, you can change the refresh rate of -the catalog in your `packages/backend/src/plugins/catalog.ts` file: +rate limiting. You can change the refresh rate of the catalog in your `packages/backend/src/plugins/catalog.ts` file: ```typescript const builder = await CatalogBuilder.create(env); @@ -112,11 +111,8 @@ const builder = await CatalogBuilder.create(env); builder.setProcessingIntervalSeconds(300); ``` -Alternatively, or additionally, you can configure [github-apps] authentication +Alternatively, or additionally, you can configure [github-apps](github-apps.md) authentication which carries a much higher rate limit at GitHub. This is true for any method of adding GitHub entities to the catalog, but especially easy to hit with automatic discovery. - -[rate limits]: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting -[github-apps]: ../../plugins/github-apps.md diff --git a/docs/plugins/github-apps.md b/docs/integrations/github/github-apps.md similarity index 92% rename from docs/plugins/github-apps.md rename to docs/integrations/github/github-apps.md index 434c9a4882..5725e67c7b 100644 --- a/docs/plugins/github-apps.md +++ b/docs/integrations/github/github-apps.md @@ -1,6 +1,6 @@ --- id: github-apps -title: GitHub Apps for Backend Authentication +title: GitHub Apps description: Configure GitHub Apps for Backend Authentication --- @@ -13,6 +13,8 @@ the OAuth apps and their respective scopes. ## Caveats +- This authentication method is built for authenticating towards + organization repositories and not towards personal repositories. - It's not possible to have multiple Backstage GitHub Apps installed in the same GitHub organization, to be handled by Backstage. We currently don't check through all the registered GitHub Apps to see which ones are installed for a @@ -25,7 +27,7 @@ the OAuth apps and their respective scopes. want for github.com but it's recommended to make your application public for GitHub Enterprise in order to share application across your GHE organizations. -A GitHub app created with `backstage-cli create-github-app` will have read +A GitHub app created with the cli will have read access by default. You have to manually update the GitHub App settings in GitHub to grant the app more permissions if needed. @@ -35,8 +37,12 @@ You can use the `backstage-cli` to create a GitHub App using a manifest file that we provide. This gives us a way to automate some of the work required to create a GitHub app. +```console +yarn backstage-cli create-github-app +``` + You can read more about the -[`backstage-cli create-github-app` method](../local-dev/cli-commands.md#create-github-app). +[`backstage-cli create-github-app`](../../local-dev/cli-commands.md#create-github-app) command. Once you've gone through the CLI command, it should produce a YAML file in the root of the project which you can then use as an `include` in your diff --git a/docs/integrations/github/locations.md b/docs/integrations/github/locations.md index 3395c61117..7d3ce632d6 100644 --- a/docs/integrations/github/locations.md +++ b/docs/integrations/github/locations.md @@ -75,8 +75,6 @@ your use of the integration: ## Authentication with GitHub Apps -Alternatively, Backstage can use GitHub Apps for backend authentication. This -has higher rate limits, and a clearer authorization model. See [github-apps] for +Alternatively for GitHub organizations, Backstage can use GitHub Apps for backend authentication. This +has higher rate limits, and a clearer authorization model. See [github-apps](github-apps.md) for how to set this up. - -[github-apps]: ../../plugins/github-apps.md diff --git a/docs/integrations/google-cloud-storage/locations.md b/docs/integrations/google-cloud-storage/locations.md index 16e19ea350..e15cb6346c 100644 --- a/docs/integrations/google-cloud-storage/locations.md +++ b/docs/integrations/google-cloud-storage/locations.md @@ -48,7 +48,7 @@ you can check [this documentation page][google gcs docs]. To use this integration to import entities from a GCS bucket go to the Google console and browse the file you would like to import. Then copy the `Authenticated URL` and paste it into the text box in the `register component` -form. This url should look like +form. This URL should look like `https://storage.cloud.google.com///catalog-info.yaml`. [google gcs docs]: https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index 7157fad2bf..715d586545 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -326,8 +326,9 @@ in the package registry, and will update entries both in `yarn.lock` and Usage: backstage-cli versions:bump [options] Options: - -h, --help display help for command - -p, --pattern Override glob for matching packages to upgrade + -h, --help display help for command + --pattern <glob> Override glob for matching packages to upgrade + --release <version|next|main> Bump to a specific Backstage release line or version (default: "main") ``` ## versions:check @@ -361,7 +362,7 @@ Usage: backstage-cli build-workspace [options] <workspace-dir> Creates a GitHub App in your GitHub organization. This is an alternative to token-based [GitHub integration](../integrations/github/locations.md). See -[GitHub Apps for Backstage Authentication](../plugins/github-apps.md). +[GitHub Apps for Backstage Authentication](../integrations/github/github-apps.md). Launches a browser to create the App through GitHub and saves the result as a YAML file that can be referenced in the GitHub integration configuration. diff --git a/docs/plugins/url-reader.md b/docs/plugins/url-reader.md index 0072c34cfb..8a84f3e1ff 100644 --- a/docs/plugins/url-reader.md +++ b/docs/plugins/url-reader.md @@ -24,7 +24,7 @@ package provides a dedicated API for reading from such URL based remote locations like GitHub, GitLab, Bitbucket, Google Cloud Storage, etc. This is commonly referred to as "URL Reader". It takes care of making authenticated requests to the remote host so that private files can be read securely. If users -have [GitHub App based authentication](github-apps.md) set up, URL Reader even +have [GitHub App based authentication](../integrations/github/github-apps.md) set up, URL Reader even refreshes the token, to avoid reaching the GitHub API rate limit. As a result, plugin authors do not have to worry about any of these problems diff --git a/docs/releases/v1.1.0-changelog.md b/docs/releases/v1.1.0-changelog.md new file mode 100644 index 0000000000..270f90b0d3 --- /dev/null +++ b/docs/releases/v1.1.0-changelog.md @@ -0,0 +1,2703 @@ +# Release v1.1.0 + +## @backstage/backend-tasks@0.3.0 + +### Minor Changes + +- ab008a0988: Adds the ability to manually trigger tasks which are registered + +### Patch Changes + +- bdd2773202: Refactored the internal `TaskWorker` class to make it easier to test. + +- a83babdd63: Fixed the `initialDelay` parameter of tasks to properly make task workers + _always_ wait before the first invocations on startup, not just the very first + time that the task is ever created. This behavior is more in line with + expectations. Callers to not need to update their code. + + Also clarified in the doc comment for the field that this wait applies only on + an individual worker level. That is, if you have a cluster of workers then each + individual machine may postpone its first task invocation by the given amount of + time to leave room for the service to settle, but _other_ workers may still + continue to invoke the task on the regular cadence in the meantime. + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/cli@0.17.0 + +### Minor Changes + +- 1f7d4763ab: **BREAKING**: Bump the version range of `jest` from `^26.0.1` to `^27.5.1`. You can find the complete list of breaking changes [here](https://github.com/facebook/jest/releases/tag/v27.0.0). + + We strongly recommend to have completed the [package role migration](https://backstage.io/docs/tutorials/package-role-migration) before upgrading to this version, as the package roles are used to automatically determine the testing environment for each package. If you instead want to set an explicit test environment for each package, you can do so for example in the `"jest"` section in `package.json`. The default test environment for all packages is now `node`, which is also the new Jest default. + + Note that one of the breaking changes of Jest 27 is that the `jsdom` environment no longer includes `setImmediate` and `clearImmediate`, which means you might need to update some of your frontend packages. Another notable change is that `jest.useFakeTimers` now defaults to the `'modern'` implementation, which also mocks microtasks. + +### Patch Changes + +- e80ecad93c: Bump the `rushstack` api generator libraries to their latest versions + +- c54ce828bd: build(deps): bump `eslint-plugin-jest` from 25.3.4 to 26.1.2 + +- f151dfee5a: build(deps): bump `eslint-webpack-plugin` from 2.6.0 to 3.1.1 + +- 7e7ba704be: build(deps): bump `@spotify/eslint-config-base` from 12.0.0 to 13.0.0 + +- ecd72391fb: build(deps): bump `@spotify/eslint-config-typescript` + +- 6a341b2d87: build(deps): bump `@spotify/eslint-config-react` from 12.0.0 to 13.0.0 + +- 3c26b2edb5: build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0 + +- ed3551b7be: Introduced a new experimental test configuration with a number of changes. It switches the coverage provider from `v8` to the default Babel provider, along with always enabling source maps in the Sucrase transform. It also adds a custom module loader that caches both file transforms and VM script objects across all projects in a test run, which provides a big performance boost when running tests from the project root, increasing speed and reducing memory usage. + + This new configuration is not enabled by default. It is enabled by setting the environment variable `BACKSTAGE_NEXT_TESTS` to a non-empty value. + +- 6ad0c45648: Added an experimental `package fix` command which applies automated fixes to the target package. The initial fix that is available is to add missing monorepo dependencies to the target package. + +- 5b3079694e: Stop logging "Stopped watcher" when shutting down the development backend. + +- f512554910: Updated the plugin template to install version 14 of `@testing-library/user-event`. + + To apply this change to your own project, update the `devDependencies` section in your `package.json` files: + + ```diff + "devDependencies": { + ... omitted dev dependencies ... + - "@testing-library/user-event": "^13.1.8", + + "@testing-library/user-event": "^14.0.0", + ... omitted dev dependencies ... + } + ``` + +- df7862cfa6: Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds. + +- 230ad0826f: Bump to using `@types/node` v16 + +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +- 948a56f401: Added a new experimental `repo list-deprecations` command, which scans the entire project for usage of deprecated APIs. + +- 4782f9e925: Fixed misleading log message during frontend plugin creation. + +- 0383cd0228: The `versions:*` commands no longer warns about duplicate plugin libraries, such as `@backstage/plugin-catalog-common`. + +- Updated dependencies + - @backstage/config-loader@1.1.0 + +## @backstage/config-loader@1.1.0 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## @backstage/integration@1.1.0 + +### Minor Changes + +- b7436743cb: Gerrit integration: Added an optional configuration to set the Gitiles base url. + +### Patch Changes + +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` + +- 3ef123bbf0: Support external ID when assuming roles in S3 integration + + In order to assume a role created by a 3rd party as external + ID is needed. This change adds an optional field to the s3 + integration configuration and consumes that in the AwsS3UrlReader. + +- d26e1b0146: Exported `replaceGitLabUrlType` from package + +## @techdocs/cli@1.1.0 + +### Minor Changes + +- 733187987b: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + + **WARNING**: If you notice 404s in TechDocs after updating, check to make sure that all markdown files referenced in your `mkdocs.yml`s' `nav` sections exist. The following flag may be passed to the `generate` command to temporarily revert to the broken behavior. + + ```sh + techdocs-cli generate --legacyCopyReadmeMdToIndexMd + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- eb470ea54c: Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-techdocs-node@1.1.0 + +## @backstage/plugin-auth-backend@0.13.0 + +### Minor Changes + +- 15d3a3c39a: **BREAKING**: All sign-in resolvers must now return a `token` in their sign-in result. Returning an `id` is no longer supported. +- c5aeaf339d: **BREAKING**: All auth providers have had their default sign-in resolvers removed. This means that if you want to use a particular provider for sign-in, you must provide an explicit sign-in resolver. For more information on how to configure sign-in resolvers, see the [sign-in resolver documentation](https://backstage.io/docs/auth/identity-resolver). + +### Patch Changes + +- c5aeaf339d: **DEPRECATION**: The `AuthProviderFactoryOptions` type has been deprecated, as the options are now instead inlined in the `AuthProviderFactory` type. This will make it possible to more easily introduce new options in the future without a possibly breaking change. + +- 794f7542b6: Updated openid-client from 4.1.2 to 5.1.3 + +- c5aeaf339d: **DEPRECATION**: The `getEntityClaims` helper has been deprecated, with `getDefaultOwnershipEntityRefs` being added to replace it. + +- de231e5b06: Declare oauth2 `clientSecret` with visibility secret + +- c5aeaf339d: **DEPRECATION**: All `createProvider` and `*SignInResolver` have been deprecated. Instead, a single `providers` object is exported which contains all built-in auth providers. + + If you have a setup that currently looks for example like this: + + ```ts + import { + createRouter, + defaultAuthProviderFactories, + createGoogleProvider, + googleEmailSignInResolver, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: createGoogleProvider({ + signIn: { + resolver: googleEmailSignInResolver, + }, + }), + }, + }); + } + ``` + + You would migrate it to something like this: + + ```ts + import { + createRouter, + providers, + defaultAuthProviderFactories, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: providers.google.create({ + signIn: { + resolver: + providers.google.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); + } + ``` + +- 2cc1d1b235: Applied the fix from version 0.12.3 of this package, which is part of the v1.0.1 release of Backstage. + +- c5aeaf339d: **DEPRECATION** The `AuthResolverContext` has received a number of changes, which is the context used by auth handlers and sign-in resolvers. + + The following fields deprecated: `logger`, `tokenIssuer`, `catalogIdentityClient`. If you need to access the `logger`, you can do so through a closure instead. The `tokenIssuer` has been replaced with an `issueToken` method, which is available directory on the context. The `catalogIdentityClient` has been replaced by the `signInWithCatalogUser` method, as well as the lower level `findCatalogUser` method and `getDefaultOwnershipEntityRefs` helper. + + It should be possible to migrate most sign-in resolvers to more or less only use `signInWithCatalogUser`, for example an email lookup resolver like this one: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + ``` + + can be migrated to the following: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + }; + ``` + + While a direct entity name lookup using a user ID might look like this: + + ```ts + async ({ result: { fullProfile } }, ctx) => { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.userId, + }, + }); + }; + ``` + + If you want more control over the way that users are looked up, ownership is assigned, or tokens are issued, you can use a combination of the `findCatalogUser`, `getDefaultOwnershipEntityRefs`, and `issueToken` instead. + +- f4cdf4cac1: Defensively encode URL parameters when fetching ELB keys + +- 6ee04078e1: **DEPRECATION**: The `tokenIssuer` option for `OAuthAdapter` is no longer needed and has been deprecated. + +- a45bce06e3: Handle trailing slashes on GitHub `enterpriseInstanceUrl` settings + +- 45f7a261c7: Bumped passport-microsoft to resolve CVE-2021-41580 + +- c5aeaf339d: Added exports of the following types: `AuthProviderConfig`, `StateEncoder`, `TokenParams`, `AwsAlbResult`. + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-auth-node@0.2.0 + +### Minor Changes + +- 15d3a3c39a: **BREAKING**: Removed the deprecated `id` and `entity` fields from `BackstageSignInResult`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog@1.1.0 + +### Minor Changes + +- bdc61b4002: Expose 'initalFilter' through initialKind prop on Catalog Page. + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 2a3cac4c60: Corrected replacements for depreciated FilteredEntityLayout items +- f6d2694092: Added tableOptions property to CatalogTable and DefaultCatalogPage to support customization of the Catalog Table. Related issue #10453 +- 5d5fdbe541: Columns in CatalogTable now change depending on the entity kind, ensuring only relevant columns are displayed. +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-catalog-backend@1.1.0 + +### Minor Changes + +- 8012ac46a0: **BREAKING (alpha api):** Replace `createCatalogPolicyDecision` export with `createCatalogConditionalDecision`, which accepts a permission parameter of type `ResourcePermission<'catalog-entity'>` along with conditions. The permission passed is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: + + ```typescript + class TestPermissionPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + _user?: BackstageIdentityResponse, + ): Promise { + if ( + // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> + isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) + ) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner( + _user?.identity.ownershipEntityRefs ?? [], + ), + ); + } + + return { + result: AuthorizeResult.ALLOW, + }; + ``` + +- 8012ac46a0: **BREAKING:** Mark CatalogBuilder#addPermissionRules as @alpha. + +- fb02d2d94d: export `locationSpecToLocationEntity` + +- bf82edf4c9: Added `/validate-entity` endpoint + +### Patch Changes + +- ada4446733: Specify type of `visibilityPermission` property on collators and collator factories. + +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` + +- 8592cacfd3: Fixed an issue where sometimes entities would have stale relations "stuck" and + not getting removed as expected, after the other end of the relation had stopped + referring to them. + +- 23646e51a5: Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions. + +- 9fe24b0fc8: Adjust the error messages when entities fail validation, to clearly state what entity that failed it + +- 48405ed232: Added `spec.profile.displayName` to search index for Group kinds + +- 95408dbe99: Enable internal batching of very large deletions, to not run into SQL binding limits + +- 8012ac46a0: Handle changes to @alpha permission-related types. + + - All exported permission rules and conditions now have a `resourceType`. + - `createCatalogConditionalDecision` now expects supplied conditions to have the appropriate `resourceType`. + - `createCatalogPermissionRule` now expects `resourceType` as part of the supplied rule object. + - Introduce new `CatalogPermissionRule` convenience type. + +- ffec894ed0: add gitlab to AnnotateScmSlugEntityProcessor + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.0 + +### Minor Changes + +- d3af30b47d: Created a module to extract the CI/CD statistics from a Gitlab repository. + Read the `README.md` in the `cicd-statistics-module-gitlab` plugin folder on how to set it up. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-cicd-statistics@0.1.6 + +## @backstage/plugin-gcalendar@0.3.0 + +### Minor Changes + +- c6616e6fc9: Fixed issue when not all calendars were fetched for some accounts + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-kubernetes-backend@0.5.0 + +### Minor Changes + +- 3d45427666: **BREAKING** Custom cluster suppliers need to cache their getClusters result + + To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters + the `getClusters` method is now called whenever the list of clusters is needed. + + Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters` + can be called frequently and should return a cached result from `getClusters` instead. + + For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`: + + ```diff + -import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; + +import { + + ClusterDetails, + + KubernetesBuilder, + + KubernetesClustersSupplier, + +} from '@backstage/plugin-kubernetes-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + +import { Duration } from 'luxon'; + + + +export class CustomClustersSupplier implements KubernetesClustersSupplier { + + constructor(private clusterDetails: ClusterDetails[] = []) {} + + + + static create(refreshInterval: Duration) { + + const clusterSupplier = new CustomClustersSupplier(); + + // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts + + runPeriodically( + + () => clusterSupplier.refreshClusters(), + + refreshInterval.toMillis(), + + ); + + return clusterSupplier; + + } + + + + async refreshClusters(): Promise { + + this.clusterDetails = []; // fetch from somewhere + + } + + + + async getClusters(): Promise { + + return this.clusterDetails; + + } + +} + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + - const { router } = await KubernetesBuilder.createBuilder({ + + const builder = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + - }).build(); + + }); + + builder.setClusterSupplier( + + CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), + + ); + + const { router } = await builder.build(); + ``` + +### Patch Changes + +- 753a20c89e: Add kubernetes namespace annotation `backstage.io/kubernetes-namespace` to allow namespaced Kubernetes resources fetches +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-kubernetes-common@0.2.9 + +## @backstage/plugin-permission-common@0.6.0 + +### Minor Changes + +- 8012ac46a0: Add `resourceType` property to `PermissionCondition` type to allow matching them with `ResourcePermission` instances. +- c98d271466: Refactor api types into more specific, decoupled names. + + - **BREAKING:** + - Renamed `AuthorizeDecision` to `EvaluatePermissionResponse` + - Renamed `AuthorizeQuery` to `EvaluatePermissionRequest` + - Renamed `AuthorizeRequest` to `EvaluatePermissionRequestBatch` + - Renamed `AuthorizeResponse` to `EvaluatePermissionResponseBatch` + - Renamed `Identified` to `IdentifiedPermissionMessage` + - Add `PermissionMessageBatch` helper type + - Add `ConditionalPolicyDecision`, `DefinitivePolicyDecision`, and `PolicyDecision` types from `@backstage/plugin-permission-node` + +### Patch Changes + +- 90754d4fa9: Removed [strict](https://github.com/colinhacks/zod#strict) validation from `PermissionCriteria` schemas to support backward-compatible changes. + +- 2b07063d77: Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: + + - `authorize()`: Used when the caller requires a definitive decision. + - `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. + +- 8012ac46a0: Add `isPermission` helper method. + +- 95284162d6: - Add more specific `Permission` types. + - Add `createPermission` helper to infer the appropriate type for some permission input. + - Add `isResourcePermission` helper to refine Permissions to ResourcePermissions. + +## @backstage/plugin-permission-node@0.6.0 + +### Minor Changes + +- 8012ac46a0: **BREAKING**: Stronger typing in `PermissionPolicy` 🎉. + + Previously, it was entirely the responsibility of the `PermissionPolicy` author to only return `CONDITIONAL` decisions for permissions that are associated with a resource, and to return the correct kind of `PermissionCondition` instances inside the decision. Now, the policy authoring helpers provided in this package now ensure that the decision and permission match. + + **For policy authors**: rename and adjust api of `createConditionExports`. Previously, the function returned a factory for creating conditional decisions named `createPolicyDecision`, which had a couple of drawbacks: + + 1. The function always creates a _conditional_ policy decision, but this was not reflected in the name. + 2. Conditional decisions should only ever be returned from `PermissionPolicy#handle` for resource permissions, but there was nothing in the API that encoded this constraint. + + This change addresses the drawbacks above by making the following changes for policy authors: + + - The `createPolicyDecision` method has been renamed to `createConditionalDecision`. + - Along with conditions, the method now accepts a permission, which must be a `ResourcePermission`. This is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: + + ```typescript + class TestPermissionPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + _user?: BackstageIdentityResponse, + ): Promise { + if ( + // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> + isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) + ) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner( + _user?.identity.ownershipEntityRefs ?? [], + ), + ); + } + + return { + result: AuthorizeResult.ALLOW, + }; + ``` + + **BREAKING**: when creating `PermissionRule`s, provide a `resourceType`. + + ```diff + export const isEntityOwner = createCatalogPermissionRule({ + name: 'IS_ENTITY_OWNER', + description: 'Allow entities owned by the current user', + + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + apply: (resource: Entity, claims: string[]) => { + if (!resource.relations) { + return false; + } + + return resource.relations + .filter(relation => relation.type === RELATION_OWNED_BY) + .some(relation => claims.includes(relation.targetRef)); + }, + toQuery: (claims: string[]) => ({ + key: 'relations.ownedBy', + values: claims, + }), + }); + ``` + +- c98d271466: **BREAKING:** + + - Rename `PolicyAuthorizeQuery` to `PolicyQuery` + - Remove `PolicyDecision`, `DefinitivePolicyDecision`, and `ConditionalPolicyDecision`. These types are now exported from `@backstage/plugin-permission-common` + +- 322b69e46a: **BREAKING:** `ServerPermissionClient` now implements `PermissionEvaluator`, which moves out the capabilities for evaluating conditional decisions from `authorize()` to `authorizeConditional()` method. + +### Patch Changes + +- 90754d4fa9: Removed [strict](https://github.com/colinhacks/zod#strict) validation from `PermissionCriteria` schemas to support backward-compatible changes. +- 8012ac46a0: Fix signature of permission rule in test suites +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-permission-react@0.4.0 + +### Minor Changes + +- 5bdcb8c45d: **BREAKING**: More restrictive typing for `usePermission` hook and `PermissionedRoute` component. It's no longer possible to pass a `resourceRef` unless the permission is of type `ResourcePermission`. + +### Patch Changes + +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 322b69e46a: **BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-scaffolder@1.1.0 + +### Minor Changes + +- 6331ec1ebc: Added a context menu to the scaffolder page that provides links to the template editor and actions reference. These links and the presence of the context menu can be toggled through the `contextMenu` prop of the scaffolder page. +- 8fad3b6ef5: The Template Preview page has been renamed to Template Editor, and is not available at the `/edit` path instead. There is a redirect in place from `/preview`. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 946af407db: Changed input label for owner field in GitlabRepoPicker +- 230ad0826f: Bump to using `@types/node` v16 +- d34900af81: Added a new `NextScaffolderRouter` which will eventually replace the exiting router +- 9975ff9852: Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. +- 4431873583: Update `usePermission` usage. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + +## @backstage/plugin-scaffolder-backend@1.1.0 + +### Minor Changes + +- 2a7d52ca2c: Override default commit message and author details in GitLab action +- f5f921dafb: Add new `draft` option to the `publish:github:pull-request` action. + +### Patch Changes + +- 64d9a031a8: build(deps): bump `isbinaryfile` from 4.0.8 to 5.0.0 +- 2f3d3a1eae: build(deps): bump `@gitbeaker/core` from 34.6.0 to 35.6.0 +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + +## @backstage/plugin-search@0.8.0 + +### Minor Changes + +- 520e21aaea: The following exports has now been fully deleted from this package and can be import from `@backstage/plugin-search-react` instead. + + `SearchApi` interface. + `searchApiRef` + `SearchContextProvider` + `useSearch` + + `SearchContext` has now been fully deleted from this package and is no longer exported publicly. Use `SearchContextProvider` when access to the context is needed. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- 5c062f275e: Support customizing the content of the `SidebarSearchModal` + +- 38e01f2f70: Switch to `SearchDocument` type in `DefaultResultListItem` props + +- 230ad0826f: Bump to using `@types/node` v16 + +- ab230a433f: The following exports has been moved to `@backstage/plugin-search-react` and will be removed in the next release. import from `@backstage/plugin-search-react` instead. + + - `SearchApi` interface. + - `searchApiRef` + - `SearchContext` + - `SearchContextProvider` + - `useSearch` + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-search-common@0.3.3 + +## @backstage/plugin-search-backend@0.5.0 + +### Minor Changes + +- 94ccd772d4: **BREAKING**: The `authorization` property is no longer returned on search results when queried. Note: this will only result in a breaking change if you have custom code in your frontend that relies on the `authorization.resourceRef` property on documents. + +### Patch Changes + +- 30f9884359: Check for non-resource permissions when authorizing result-by-result in AuthorizedSearchEngine. +- 3c8cfaaa80: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-search-backend-node@0.6.0 + +## @backstage/plugin-search-backend-node@0.6.0 + +### Minor Changes + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/backend-tasks@0.3.0 + - @backstage/plugin-search-common@0.3.3 + +## @backstage/plugin-search-react@0.1.0 + +### Minor Changes + +- ab230a433f: New search package to hold things the search plugin itself and other frontend plugins (e.g. techdocs, home) depend on. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +- 076b091113: api-report clean up - the package now exports following additional types: + + `SearchContextProviderProps` + `SearchContextValue` + `SearchContextProviderForStorybookProps` + `SearchApiProviderForStorybookProps` + +- e1de8526aa: Versioned search context managed through version-bridge + +- Updated dependencies + - @backstage/core-app-api@1.0.1 + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-search-common@0.3.3 + +## @backstage/plugin-stack-overflow@0.1.0 + +### Minor Changes + +- ac323de4ad: Add stack overflow plugin + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-home@0.4.20 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-common@0.3.3 + +## @backstage/plugin-stack-overflow-backend@0.1.0 + +### Minor Changes + +- ac323de4ad: Add stack overflow backend plugin + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + +## @backstage/plugin-tech-insights@0.2.0 + +### Minor Changes + +- 567b13a84a: Add checksId option to EntityTechInsightsScorecardContent component + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- 230ad0826f: Bump to using `@types/node` v16 + +- 2fe58c7285: Improved the Tech-Insights documentation: + + - `lifecycle` examples used `ttl` when it should be `timeToLive` + - Added list of included FactRetrievers + - Added full backend example using all included FactRetrievers + - Added boolean scorecard example image showing results of backend example + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-tech-insights-backend@0.3.0 + +### Minor Changes + +- 231fee736b: This backend now uses the `@backstage/backend-tasks` package facilities for scheduling fact retrievers. + + **BREAKING**: The `buildTechInsightsContext` function now takes an additional field in its options argument: `scheduler`. This is an instance of `PluginTaskScheduler`, which can be found in your backend initialization code's `env`. + + ```diff + const builder = buildTechInsightsContext({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + + scheduler: env.scheduler, + factRetrievers: [ /* ... */ ], + }); + ``` + +### Patch Changes + +- 21de525ce9: Updated README.md with better install instructions + +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +- 2fe58c7285: Improved the Tech-Insights documentation: + + - `lifecycle` examples used `ttl` when it should be `timeToLive` + - Added list of included FactRetrievers + - Added full backend example using all included FactRetrievers + - Added boolean scorecard example image showing results of backend example + +- Updated dependencies + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-techdocs@1.1.0 + +### Minor Changes + +- ace749b785: TechDocs supports a new, experimental method of customization: addons! + + To customize the standalone TechDocs reader page experience, update your `/packages/app/src/App.tsx` in the following way: + + ```diff + import { TechDocsIndexPage, TechDocsReaderPage } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + } /> + } + > + + + + + + + + + // ... + ``` + + To customize the TechDocs reader experience on the Catalog entity page, update your `packages/app/src/components/catalog/EntityPage.tsx` in the following way: + + ```diff + import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + + import { SomeAddon } from '@backstage/plugin-some-plugin'; + + // ... + + + + {overviewContent} + + + + - + + + + + + + + + + + + + + // ... + ``` + + If you do not wish to customize your TechDocs reader experience in this way at this time, no changes are necessary! + +### Patch Changes + +- ab230a433f: imports from `@backstage/plugin-search-react` instead of `@backstage/plugin-search` + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- 230ad0826f: Bump to using `@types/node` v16 + +- f0fb9153b7: Fix broken query selectors on techdocs + +- 9975ff9852: Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. + +- 3ba256c389: Fixed a bug preventing custom TechDocs reader page implementations from rendering without being double-wrapped in the `` component. + +- fe53fe97d7: Fix permalink scrolling for anchors where the id starts with a number. + +- 0152c0de22: Some documentation layout tweaks: + + - drawer toggle margins + - code block margins + - sidebar drawer width + - inner content width + - footer link width + - sidebar table of contents scroll + +- 3ba256c389: Fixed a bug that caused addons in the `Subheader` location to break the default TechDocs reader page layout. + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-techdocs-react@0.1.0 + - @backstage/integration-react@1.0.1 + +## @backstage/plugin-techdocs-backend@1.1.0 + +### Minor Changes + +- 733187987b: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + + **WARNING**: If you notice 404s in TechDocs after updating, check to make sure that all markdown files referenced in your `mkdocs.yml`s' `nav` sections exist. The following configuration may be used to temporarily revert to the broken behavior. + + ```yaml + techdocs: + generator: + mkdocs: + legacyCopyReadmeMdToIndexMd: true + ``` + +### Patch Changes + +- ada4446733: Specify type of `visibilityPermission` property on collators and collator factories. +- 7762d54200: Fixed a bug affecting those with cache enabled that would result in empty content being cached if the first attempt to load a static asset from storage were made via a `HEAD` request, rather than a `GET` request. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/plugin-techdocs-node@1.1.0 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-techdocs-node@1.1.0 + +### Minor Changes + +- bcf1a2496c: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + +### Patch Changes + +- 9fa68705bf: The default container version used to generate TechDocs content is now `v1.0.2`. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-techdocs-react@0.1.0 + +### Minor Changes + +- ff1cc8bced: This package will house frontend utilities related to TechDocs to be shared across other frontend Backstage packages. + + In this release, it introduces a framework that can be used create TechDocs addons. + + Note: this package is not necessarily stable yet. After iteration on this package, its stability will be signaled by a major-version bump. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + +## @backstage/app-defaults@1.0.1 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/backend-common@0.13.2 + +### Patch Changes + +- 19f6c6c32a: The backend will no longer fail to start up when configured secrets do not match the configuration schema. + +- b7436743cb: Added the GerritUrlReader that implements "readUrl". + +- 3ef123bbf0: Support external ID when assuming roles in S3 integration + + In order to assume a role created by a 3rd party as external + ID is needed. This change adds an optional field to the s3 + integration configuration and consumes that in the AwsS3UrlReader. + +- bae9359032: The logger returned from `getVoidLogger` is now uses a silenced console transport instead. + +- 3ff0e79654: Tweaked the `UrlReader` multiplexer so that it uses the more helpful `NotAllowedError` messaging for all methods. + +- 12608f8ba8: Add `@types/webpack-env` to dependencies. + +- f9f512559b: Updated the visibility of database connection fields in config to be secret + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/config-loader@1.1.0 + +## @backstage/backend-test-utils@0.1.23 + +### Patch Changes + +- 0654c87cf2: `TestDatabases.create` will no longer set up an `afterAll` test handler if no databases are supported. +- Updated dependencies + - @backstage/cli@0.17.0 + - @backstage/backend-common@0.13.2 + +## @backstage/catalog-client@1.0.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + +## @backstage/catalog-model@1.0.1 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- c12f8374d1: Added a connection between www-artist and artist-lookup components + +
+ Screenshots + + ![www-artist component](https://user-images.githubusercontent.com/74687/161088772-bc1df84e-28c1-4b08-b914-109cd6d04521.jpg) + + ![artist-lookup component](https://user-images.githubusercontent.com/74687/161088775-120c5610-f094-4cd1-bb2d-3ab9623a6c72.jpg) + +
+ +## @backstage/codemods@0.1.37 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + +## @backstage/core-app-api@1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. +- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + +## @backstage/core-components@0.9.3 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 25b8e8d5b5: Add BackstageTab to overridableComponents so can override styles in a theme +- 230ad0826f: Bump to using `@types/node` v16 +- 41fd107189: Exported `IdentityProviders` type. +- a13604b8f7: Adding a name to the core-components Tab styles so can customise in the theme settings +- 19648d5cf5: fix support config ref to use backstage/backstage +- d505e43ffc: Fix highlighting of active sidebar items. +- 72f3dfd05a: Updated ProxiedSignInPageProps docs +- 7741e47eae: `` now accepts additional props `sidebarOptions` and `submenuOptions` to allow further customization +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + +## @backstage/core-plugin-api@1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/version-bridge@1.0.1 + +## @backstage/create-app@0.4.26 + +### Patch Changes + +- 1691c6c5c2: Made `User` and `Group` entity kinds not permitted by the default + `catalog.rules` config. + + The effect of this is that after creating a new Backstage repository, its + catalog no longer permits regular users to register `User` or `Group` entities + using the Backstage interface. Additionally, if you have config locations that + result in `User` or `Group` entities, you need to add those kinds to its own + specific rules: + + ```yaml + catalog: + locations: + # This applies for example to url type locations + - type: url + target: https://example.com/org.yaml + rules: + - allow: [User, Group] + # But also note that this applies to ALL org location types! + - type: github-org + target: https://github.com/my-org-name + rules: + - allow: [User, Group] + ``` + + This rule change does NOT affect entity providers, only things that are emitted + by entity processors. + + We recommend that this change is applied to your own Backstage repository, since + it makes it impossible for regular end users to affect your org data through + e.g. YAML files. To do so, remove the two kinds from the default rules in your config: + + ```diff + catalog: + rules: + - - allow: [Component, System, API, Group, User, Resource, Location] + + - allow: [Component, System, API, Resource, Location] + ``` + + And for any location that in any way results in org data being ingested, add the corresponding rule to it: + + ```diff + catalog: + locations: + - type: github-org + target: https://github.com/my-org-name + + rules: + + - allow: [User, Group] + ``` + +- 0e911394d2: Remove the `knex` package that is installed in the `packages/backend` as it's provided by the `@backstage/*` packages for you automatically. You can make the following change in your `packages/backend/package.json` if you wish to apply this change. + + ```diff + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + - "migrate:create": "knex migrate:make -x ts" + ``` + + ```diff + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + - "knex": "^0.21.6", + "pg": "^8.3.0", + ``` + +- 520e21aaea: imports `useSearch` hook from new `@backstage/plugin-search-react` package. + + To upgrade existing Apps: + + 1. Change the import to the following: + + `packages/app/src/components/search/SearchPage.tsx` + + ```diff + import { + ... + SearchType, + - useSearch, + } from '@backstage/plugin-search'; + +import { useSearch } from '@backstage/plugin-search-react'; + ``` + + 2. Add `@backstage/plugin-search-react` as a dependency to the app. + +- 43759dd789: Removed `@octokit/rest` and `@gitbeaker/node` from backend dependencies as these are unused in the default app. + + To apply these changes to your existing app, remove the following lines from the `dependencies` section of `packages/backend/package.json` + + ```diff + "@backstage/plugin-techdocs-backend": "^1.0.0", + - "@gitbeaker/node": "^34.6.0", + - "@octokit/rest": "^18.5.3", + ``` + +- e838a7060a: Add type resolutions for `@types/react` and `types/react-dom`. + + The reason for this is the usage of `"@types/react": "*"` as a dependency which is very common practice in react packages. This recently resolves to react 18 which introduces several breaking changes in both internal and external packages. + + To apply these changes to your existing installation, add a resolutions block to your `package.json` + + ```json + "resolutions": { + "@types/react": "^17", + "@types/react-dom": "^17" + }, + ``` + + If your existing app depends on react 16, use this resolution block instead. + + ```json + "resolutions": { + "@types/react": "^16", + "@types/react-dom": "^16" + }, + ``` + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +- c07d9f9e1c: Add helpful README.md files in the original `packages` and `plugins` folders + +- 230ad0826f: Bump to using `@types/node` v16 + +- 1882dbda2b: Accept `PermissionEvaluator` instead of the deprecated `PermissionAuthorizer`. + + Apply the following to `packages/backend/src/types.ts`: + + ```diff + - import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; + + import { PermissionEvaluator } from '@backstage/plugin-permission-common'; + + export type PluginEnvironment = { + ... + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + scheduler: PluginTaskScheduler; + - permissions: PermissionAuthorizer; + + permissions: PermissionEvaluator; + }; + ``` + +- e80cca164d: Tweaked `.eslintrc.js` files in the template to avoid having them apply during development. This change does not affect create apps. + +## @backstage/dev-utils@1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/test-utils@1.0.1 + - @backstage/app-defaults@1.0.1 + +## @backstage/integration-react@1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/search-common@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + +## @backstage/techdocs-common@0.11.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.1.0 + +## @backstage/test-utils@1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/core-app-api@1.0.1 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/version-bridge@1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +## @backstage/plugin-airbrake@0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/dev-utils@1.0.1 + - @backstage/test-utils@1.0.1 + +## @backstage/plugin-airbrake-backend@0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-allure@0.1.20 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-analytics-module-ga@0.1.15 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-apache-airflow@0.1.12 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-api-docs@0.8.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- ae321bd414: build(deps): bump `@asyncapi/react-component` +- 230ad0826f: Bump to using `@types/node` v16 +- 1c8ebf7af2: Add dedicated gRPC api definition widget +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog@1.1.0 + +## @backstage/plugin-app-backend@0.3.31 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.1.0 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-azure-devops@0.1.20 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-azure-devops-backend@0.3.10 + +### Patch Changes + +- 236245d9f8: Stop loading all teams when plugin is initialized +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-badges@0.2.28 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-badges-backend@0.1.25 + +### Patch Changes + +- 4c93fd36ee: allow overriding `DefaultBadgeBuilder.getMarkdownCode` +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-bazaar@0.1.19 + +### Patch Changes + +- 69738379c7: Pass authorization header with Backstage token to backend requests. +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/cli@0.17.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog@1.1.0 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-bazaar-backend@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + - @backstage/backend-test-utils@0.1.23 + +## @backstage/plugin-bitrise@0.1.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.4 + +### Patch Changes + +- 5969c4b65c: Add a new provider `AwsS3EntityProvider` as replacement for `AwsS3DiscoveryProcessor`. + + In order to migrate from the `AwsS3DiscoveryProcessor` you need to apply + the following changes: + + **Before:** + + ```yaml + # app-config.yaml + + catalog: + locations: + - type: s3-discovery + target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/ + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors ... */ + builder.addProcessor(new AwsS3DiscoveryProcessor(env.reader)); + ``` + + **After:** + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + yourProviderId: # identifies your dataset / provider independent of config changes + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors and/or providers ... */ + builder.addEntityProvider( + ...AwsS3EntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), + ); + ``` + + For simple setups, you can omit the provider ID at the config + which has the same effect as using `default` for it. + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + # uses "default" as provider ID + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + +- 776a91ea3a: Corrected title and URL to setup documentation in README + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.1.2 + +### Patch Changes + +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- 9fed130139: split BitbucketClient into BitbucketCloudClient, BitbucketServerClient +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog-backend-module-github@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-catalog-backend-module-ldap@0.4.2 + +### Patch Changes + +- ebc0aef2c6: Changed relationship creation in LDAP data processor to leverage entityRefs instead of metadata.name +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.3.1 + +### Patch Changes + +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` +- 85fc53df95: Now plugin configuration accept a new optional parameter `groupSelect` which allow the client to fetch defined fields from the ms-graph api. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + +## @backstage/plugin-catalog-common@1.0.1 + +### Patch Changes + +- ada4446733: Use `createPermission` helper when creating permissions. +- 8c8bee47f4: Add `@alpha` `CatalogEntityPermission` convenience type, available for import from `@backstage/plugin-catalog-common/alpha`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/search-common@0.3.3 + +## @backstage/plugin-catalog-graph@0.2.16 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 77800a32dd: Added renderNode and renderLabel property to EntityRelationsGraph to support customization using CustomNode and CustomLabel components +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-catalog-graphql@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + +## @backstage/plugin-catalog-import@0.8.7 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-catalog-react@1.0.1 + +### Patch Changes + +- 0ffd88a90e: Prevent permissions with types other than `ResourcePermission<'catalog-entity'>` from being used with the `useEntityPermission` hook. + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- 4af82967f4: Decouple tags picker from backend entities + + `EntityTagPicker` fetches all the tags independently and it doesn't require all the entities to be available client side. + +- 4be0d1e777: Changed catalog filter components to only pay attention to query parameters relevant to the component. + +- 5d5fdbe541: Columns in CatalogTable now change depending on the entity kind, ensuring only relevant columns are displayed. + +- 863e7bcb7b: Updated the "unregister location" behavior in `UnregisterEntityDialog`. Removed unnecessary entity deletion requests that were sent after successfully deleting a location. + +- 37b04b5a5e: Removed broken link from Labels section of entity inspector. + +- a496cee4d1: Add support for string refs to the `EntityRefLinks` component + +- d34900af81: Added a new `NextScaffolderRouter` which will eventually replace the exiting router + +- 99063c39ae: Minor API report cleanup + +- 4431873583: Update `usePermission` usage. + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-cicd-statistics@0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-circleci@0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-cloudbuild@0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-code-climate@0.1.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-code-coverage@0.1.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-code-coverage-backend@0.1.29 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-config-schema@0.1.27 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-cost-insights@0.11.26 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-explore@0.3.35 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-explore-react@0.0.16 + +## @backstage/plugin-explore-react@0.0.16 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-firehydrant@0.1.21 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-fossa@0.2.36 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-gcp-projects@0.3.23 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-git-release-manager@0.3.17 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-github-actions@0.5.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-github-deployments@0.1.35 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + +## @backstage/plugin-gitops-profiles@0.3.22 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-gocd@0.1.10 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 2677db7c15: Add pagination controls to GoCD build table +- 4c6c2b2918: Add DORA metrics insights to GoCD builds page +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-graphiql@0.2.36 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-graphql-backend@0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-graphql@0.3.8 + +## @backstage/plugin-home@0.4.20 + +### Patch Changes + +- ab230a433f: Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package. + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- ac323de4ad: - Adds new `HomePageStackOverflowQuestions` component which renders a list of stack overflow questions on your homepage. + + - Exports `ComponentRenderer` type. + +- 230ad0826f: Bump to using `@types/node` v16 + +- 9b58775849: Updated the dependency on `@backstage/config` to `^1.0.0`. + +- 99063c39ae: Minor API report cleanup + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-stack-overflow@0.1.0 + +## @backstage/plugin-ilert@0.1.30 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-jenkins@0.7.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-jenkins-common@0.1.3 + +## @backstage/plugin-jenkins-backend@0.1.20 + +### Patch Changes + +- 1917923ab8: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- b013de3f50: feature: provide access token to JenkinsInstanceConfig. It can be passed to other backend calls if authentication enabled. DefaultJenkinsInfoProvider sends always this token to catalog api if access token exists. +- ca91107110: Fixed possible type error if jenkins response contains null values +- 71f8708f00: Make `resourceRef` required in `JenkinsApi` to match usage. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-jenkins-common@0.1.3 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-jenkins-common@0.1.3 + +### Patch Changes + +- ada4446733: Use `createPermission` helper when creating permissions. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-catalog-common@1.0.1 + +## @backstage/plugin-kafka@0.3.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-kafka-backend@0.2.24 + +### Patch Changes + +- f26cf63878: build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0 +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-kubernetes@0.6.4 + +### Patch Changes + +- 82e2ae6ff6: build(deps): bump `cronstrue` from 1.125.0 to 2.2.0 +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 1023ee6353: export kubernetes components +- 230ad0826f: Bump to using `@types/node` v16 +- f616d99f6f: Fix division by zero in currentToDeclaredResourceToPerc when pod resources weren't set +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-kubernetes-common@0.2.9 + +## @backstage/plugin-kubernetes-common@0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + +## @backstage/plugin-lighthouse@0.3.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-newrelic@0.3.22 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-newrelic-dashboard@0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-org@0.5.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- cb592bfce7: Provides the ability to hide the relations toggle on the `OwnershipCard` as well as setting a default relation type. + + To hide the toggle simply include the `hideRelationsToggle` prop like this: + + ```tsx + + ``` + + To set the default relation type, add the `relationsType` prop with a value of direct or aggregated, the default if not provided is direct. Here is an example: + + ```tsx + + ``` + +- d014fe2cb4: Introduced a new MyGroupsSidebarItem SidebarItem that links to one or more groups based on the logged in user's membership. + + To use MyGroupsSidebarItem you'll need to add it to your `Root.tsx` like this: + + ```diff + // app/src/components/Root/Root.tsx + + import { MyGroupsSidebarItem } from '@backstage/plugin-org'; + + import GroupIcon from '@material-ui/icons/People'; + + + + //... + }> + {/* Global nav, not org-specific */} + //... + + + + //... + + + + ``` + +- 111995470d: add aggregated ownership type for kind group in OwnershipCard + +- 230ad0826f: Bump to using `@types/node` v16 + +- 0bada4fc4d: Added the `metadata.description` to the bottom of each member on the MembersListCard + +- 99063c39ae: Minor API report cleanup + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-pagerduty@0.3.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-periskop@0.1.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-periskop-backend@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-permission-backend@0.5.6 + +### Patch Changes + +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 95284162d6: - Add more specific check for policies which return conditional decisions for non-resource permissions. + - Refine permission validation in authorize endpoint to differentiate between `BasicPermission` and `ResourcePermission` instances. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-proxy-backend@0.2.25 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-rollbar@0.4.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-rollbar-backend@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.3.6 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + +## @backstage/plugin-scaffolder-common@1.0.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + +## @backstage/plugin-search-backend-module-elasticsearch@0.1.3 + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-search-backend-node@0.6.0 + +## @backstage/plugin-search-backend-module-pg@0.3.2 + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-search-backend-node@0.6.0 + +## @backstage/plugin-search-common@0.3.3 + +### Patch Changes + +- c8b295f2fb: - Introduce `SearchDocument` type. This type contains the subset of `IndexableDocument` properties relevant to the frontend, and is intended to be used for documents returned to the frontend from the search API. + - `SearchResultSet` is now a wrapper for documents of type `SearchDocument`, and is intended to be used in the frontend. This isn't a breaking change, since `IndexableDocument`s are valid `SearchDocument`s, so the old and new types are compatible. + - Introduce `IndexableResultSet` type, which wraps `IndexableDocument` instances in the same way as `SearchResultSet`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + +## @backstage/plugin-sentry@0.3.42 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 68bb66102d: Exposed options for the material table +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-shortcuts@0.2.5 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-sonarqube@0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-splunk-on-call@0.3.28 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.15 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- ab008a0988: Removes node-cron from tech-insights to utilize backend-tasks +- Updated dependencies + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-tech-insights-node@0.2.9 + +### Patch Changes + +- 231fee736b: Adds an optional timeout to fact retriever registrations to stop a task if it runs too long. +- Updated dependencies + - @backstage/backend-common@0.13.2 + +## @backstage/plugin-tech-radar@0.5.11 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- f69789330a: Fix an issue where the Radar is not updated when switching between different radars +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-todo@0.2.6 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-todo-backend@0.1.28 + +### Patch Changes + +- 5da036264b: Fix method to get source-location. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## @backstage/plugin-user-settings@0.4.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 + +- cf647f6a72: Wired up the OneLogin provider to be visible in the Settings UI when configured correctly. + + Previously it wasn't visible at all. + +- af508a895e: Dynamically layout User Settings theme toggle container + +- 230ad0826f: Bump to using `@types/node` v16 + +- 016c574b51: Added the ability to render extra setting tabs + +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## @backstage/plugin-xcmetrics@0.2.24 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## example-app@0.2.70 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-home@0.4.20 + - @backstage/plugin-techdocs@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/cli@0.17.0 + - @backstage/plugin-scaffolder@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-kubernetes@0.6.4 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog-graph@0.2.16 + - @backstage/plugin-catalog-import@0.8.7 + - @backstage/plugin-kafka@0.3.4 + - @backstage/plugin-lighthouse@0.3.4 + - @backstage/plugin-rollbar@0.4.4 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-search@0.8.0 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-airbrake@0.3.4 + - @backstage/plugin-apache-airflow@0.1.12 + - @backstage/plugin-api-docs@0.8.4 + - @backstage/plugin-azure-devops@0.1.20 + - @backstage/plugin-badges@0.2.28 + - @backstage/plugin-catalog@1.1.0 + - @backstage/plugin-circleci@0.3.4 + - @backstage/plugin-cloudbuild@0.3.4 + - @backstage/plugin-code-coverage@0.1.31 + - @backstage/plugin-cost-insights@0.11.26 + - @backstage/plugin-explore@0.3.35 + - @backstage/plugin-gcalendar@0.3.0 + - @backstage/plugin-gcp-projects@0.3.23 + - @backstage/plugin-github-actions@0.5.4 + - @backstage/plugin-gocd@0.1.10 + - @backstage/plugin-graphiql@0.2.36 + - @backstage/plugin-jenkins@0.7.3 + - @backstage/plugin-newrelic@0.3.22 + - @backstage/plugin-org@0.5.4 + - @backstage/plugin-pagerduty@0.3.31 + - @backstage/plugin-sentry@0.3.42 + - @backstage/plugin-shortcuts@0.2.5 + - @backstage/plugin-stack-overflow@0.1.0 + - @backstage/plugin-tech-insights@0.2.0 + - @backstage/plugin-tech-radar@0.5.11 + - @backstage/plugin-todo@0.2.6 + - @backstage/plugin-user-settings@0.4.3 + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/app-defaults@1.0.1 + - @backstage/plugin-newrelic-dashboard@0.1.12 + +## example-backend@0.2.70 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/plugin-techdocs-backend@1.1.0 + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/plugin-search-backend@0.5.0 + - @backstage/plugin-auth-backend@0.13.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.15 + - @backstage/plugin-kafka-backend@0.2.24 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/plugin-jenkins-backend@0.1.20 + - @backstage/plugin-badges-backend@0.1.25 + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/plugin-todo-backend@0.1.28 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-kubernetes-backend@0.5.0 + - @backstage/plugin-search-backend-node@0.6.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.3 + - @backstage/plugin-search-backend-module-pg@0.3.2 + - @backstage/plugin-permission-backend@0.5.6 + - @backstage/plugin-tech-insights-backend@0.3.0 + - @backstage/plugin-azure-devops-backend@0.3.10 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.6 + - example-app@0.2.70 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-app-backend@0.3.31 + - @backstage/plugin-code-coverage-backend@0.1.29 + - @backstage/plugin-graphql-backend@0.1.21 + - @backstage/plugin-proxy-backend@0.2.25 + - @backstage/plugin-rollbar-backend@0.1.28 + +## techdocs-cli-embedded-app@0.2.69 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.1.0 + - @backstage/cli@0.17.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-techdocs-react@0.1.0 + - @backstage/integration-react@1.0.1 + - @backstage/test-utils@1.0.1 + - @backstage/plugin-catalog@1.1.0 + - @backstage/app-defaults@1.0.1 diff --git a/docs/releases/v1.1.0.md b/docs/releases/v1.1.0.md new file mode 100644 index 0000000000..0417423247 --- /dev/null +++ b/docs/releases/v1.1.0.md @@ -0,0 +1,70 @@ +--- +id: v1.1.0 +title: v1.1.0 +description: Backstage Release v1.1.0 +--- + +These are the release notes for the v1.1.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### Auth + +The auth [backend](https://github.com/backstage/backstage/tree/master/plugins/auth-backend) and [node package](https://github.com/backstage/backstage/tree/master/plugins/auth-node) received a few **BREAKING** changes. + +You are now required to always return a `token` from the result of a sign-in resolver, and all default sign-in resolvers have been removed. This means that you will have to make a conscious choice to use one of a few predefined resolver functions to be able to use a given auth provider for signing in, or to write your own. Don’t worry, it’s actually rather straightforward to do, and ends up clarifying responsibilities for safe authentication and catalog ownership. + +There are also a number of deprecations among the exported auth backend types, which you may want to adapt to. + +For more information, see the relevant [documentation section](https://backstage.io/docs/auth/identity-resolver) about sign-in resolvers, and the [backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/CHANGELOG.md#0130)/[node](https://github.com/backstage/backstage/blob/master/plugins/auth-node/CHANGELOG.md#020) changelogs. + +### CLI + +The [CLI](https://github.com/backstage/backstage/tree/master/packages/cli) has bumped the `jest` version from `^26.0.1` to `^27.5.1`. You can find the complete list of breaking changes [here](https://github.com/facebook/jest/releases/tag/v27.0.0). + +We strongly recommend having completed the [package role migration](https://backstage.io/docs/tutorials/package-role-migration) before upgrading to this version, as the package roles are used to automatically determine the testing environment for each package. + +Note that one of the breaking changes of Jest 27 is that the `jsdom` environment no longer includes `setImmediate` and `clearImmediate`, which means you might need to update some of your frontend packages. Another notable change is that `jest.useFakeTimers` now defaults to the `'modern'` implementation, which also mocks microtasks. + +### Kubernetes + +The [Kubernetes backend](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend) has a **BREAKING** change, in that cluster suppliers now need to cache their results to allow frequent calls to their methods. This was done to pave the way for custom suppliers that dynamically fetch results from the real authority, instead of hard coding them in config. Thanks [@heyLu](https://github.com/heyLu)! ([#10428](https://github.com/backstage/backstage/pull/10428)) + +### Permissions + +The permission related packages received a number of **BREAKING** changes. Among other things, the names of types were settled to be made more crisp and clear. This will mostly apply to those who write custom permissions. This all goes to the [common](https://github.com/backstage/backstage/blob/master/plugins/permission-common/CHANGELOG.md#060), [node](https://github.com/backstage/backstage/blob/master/plugins/permission-node/CHANGELOG.md#060), and [react](https://github.com/backstage/backstage/blob/master/plugins/permission-react/CHANGELOG.md#040) packages, whose changelogs are linked here. + +### Stack Overflow + +There’s a new Stack Overflow [frontend](https://github.com/backstage/backstage/tree/master/plugins/stack-overflow) plugin with a corresponding [backend](https://github.com/backstage/backstage/tree/master/plugins/stack-overflow-backend]! + +### Misc + +- The [CI/CD Statistics plugin](https://github.com/backstage/backstage/tree/master/plugins/cicd-statistics) now has GitLab support, through the new [GitLab module](https://github.com/backstage/backstage/tree/master/plugins/cicd-statistics-module-gitlab). Thanks [@djamaile](https://github.com/djamaile)! ([#10140](https://github.com/backstage/backstage/pull/10140)) + +- The catalog [AWS module](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-aws) added a new `AwsS3EntityProvider` as a replacement for the `AwsS3DiscoveryProcessor`. Thanks [@pjungermann](https://github.com/pjungermann)! ([#10480](https://github.com/backstage/backstage/pull/10480)) + +- The permissions-related exports from the catalog backend have some [breaking changes](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/CHANGELOG.md#110). These are all in alpha still, so this only applies to you if you are developing catalog permissions and import from `@backstage/plugin-catalog-backend/alpha`. + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.1.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.2.0-next.0-changelog.md b/docs/releases/v1.2.0-next.0-changelog.md new file mode 100644 index 0000000000..3ea7d86a22 --- /dev/null +++ b/docs/releases/v1.2.0-next.0-changelog.md @@ -0,0 +1,1115 @@ +# Release v1.2.0-next.0 + +## @backstage/integration@1.2.0-next.0 + +### Minor Changes + +- 6673babab9: Gerrit UrlReader: Implemented `readTree` + +- 1b4e1e2306: Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer` + (backwards compatible). + + In order to migrate to the new integration configs, + move your configs from `integrations.bitbucket` + to `integrations.bitbucketCloud` or `integrations.bitbucketServer`. + + Migration example: + + **Before:** + + ```yaml + integrations: + bitbucket: + - host: bitbucket.org + username: bitbucket_user + appPassword: app-password + - host: bitbucket-server.company.com + token: my-token + ``` + + **After:** + + ```yaml + integrations: + bitbucketCloud: + - username: bitbucket_user + appPassword: app-password + bitbucketServer: + - host: bitbucket-server.company.com + token: my-token + ``` + +- 566407bf8a: Gerrit Integration: Added the `getGerritProjectsApiUrl` function + +## @backstage/integration-react@1.1.0-next.0 + +### Minor Changes + +- 1b4e1e2306: Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer` + (backwards compatible). + + In order to migrate to the new integration configs, + move your configs from `integrations.bitbucket` + to `integrations.bitbucketCloud` or `integrations.bitbucketServer`. + + Migration example: + + **Before:** + + ```yaml + integrations: + bitbucket: + - host: bitbucket.org + username: bitbucket_user + appPassword: app-password + - host: bitbucket-server.company.com + token: my-token + ``` + + **After:** + + ```yaml + integrations: + bitbucketCloud: + - username: bitbucket_user + appPassword: app-password + bitbucketServer: + - host: bitbucket-server.company.com + token: my-token + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-catalog@1.2.0-next.0 + +### Minor Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute +- ceca63d149: Added the prop `NotFoundComponent` to `EntityLayout` which can be used to include a custom component when an entity is not found in the catalog + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.0-next.0 + +### Minor Changes + +- 566407bf8a: Initial version of the `plugin-catalog-backend-module-gerrit` plugin + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## @backstage/plugin-catalog-react@1.1.0-next.0 + +### Minor Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-scaffolder@1.2.0-next.0 + +### Minor Changes + +- 06ab5218f9: Add new bitbucket(Cloud/Server) integrations to ScaffolderClient. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## @backstage/plugin-scaffolder-backend@1.2.0-next.0 + +### Minor Changes + +- 9818112d12: Update the `github:publish` action to allow passing required status check + contexts before merging to the main branch. +- 8d5a2238a9: Split `publish:bitbucket` into `publish:bitbucketCloud` and `publish:bitbucketServer`. + + In order to migrate from the deprecated action, you need to replace the use of action + `publish:bitbucket` in your templates with the use of either `publish:bitbucketCloud` + or `publish:bitbucketServer` - depending on which destination SCM provider you use. + + Additionally, these actions will not utilize `integrations.bitbucket` anymore, + but `integrations.bitbucketCloud` or `integrations.bitbucketServer` respectively. + You may or may not have migrated to these already. + + As described in a previous changeset, using these two replacement integrations configs + will not compromise use cases which still rely on `integrations.bitbucket` as this was + set up in a backwards compatible way. + + Additionally, please mind that the option `enableLFS` is only available (and always was) + for Bitbucket Server use cases and therefore, is not even part of the schema for + `publish:bitbucketCloud` anymore. + +### Patch Changes + +- 0fc65cbf89: Override default commit message and author details in GitHub, Azure, bitbucket +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@0.1.0-next.0 + +### Minor Changes + +- 5f4dbd2b52: A package for contributed TechDocs addons. + + In this release it will introduce the ReportIssue addon, which lets you select text and open a GitHub/Gitlab issue. + +### Patch Changes + +- 075a9a067b: Updated the return type of `createTechDocsAddonExtension` to better reflect the fact that passing children to Addon components is not a valid use-case. +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + +## @backstage/backend-common@0.13.3-next.0 + +### Patch Changes + +- 6673babab9: Gerrit integration: Added optional `cloneUrl` string to config. +- 75bf9e1da9: Split BitbucketUrlReader into BitbucketCloudUrlReader and BitbucketServerUrlReader. Backwards compatible. +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9ec4e0613e: Update to `jose` 4.6.0 +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/config-loader@1.1.1-next.0 + +## @backstage/backend-tasks@0.3.1-next.0 + +### Patch Changes + +- cfd779a9bc: Scheduled tasks now have an optional `scope` field. If unset, or having the + value `'global'`, the old behavior with cross-worker locking is retained. If + having the value `'local'`, there is no coordination across workers and the + behavior is more like `setInterval`. This can be used to replace usages of + `runPeriodically` helpers. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/backend-test-utils@0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/cli@0.17.1-next.0 + +## @backstage/cli@0.17.1-next.0 + +### Patch Changes + +- 97cce67ac7: Add instructions to `create-github-app` command. +- 08e12a3a14: Add package global-agent to support behind a proxy for backstage-cli commands like versions:bump. +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 344ea56acc: Bump `commander` to version 9.1.0 +- 8ab2a8226b: Updated the `create-github-app` command to throw an error if the organization argument is a user or a non existing organization. +- Updated dependencies + - @backstage/config-loader@1.1.1-next.0 + +## @backstage/codemods@0.1.38-next.0 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 + +## @backstage/config-loader@1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully + +## @backstage/create-app@0.4.27-next.0 + +### Patch Changes + +- 3983940a21: Optimized the command order in `packages/backend/Dockerfile` as well as added the `--no-install-recommends` to the `apt-get install` and tweaked the installed packages. + + To apply this change to an existing app, update your `packages/backend/Dockerfile` to match the documented `Dockerfile` at . + +- 28bbf5aff6: Added some instruction comments to the generated config files, to clarify the + usage of `backend.baseUrl` and `backend.listen.host`. Importantly, it also per + default now listens on all IPv4 interfaces, to make it easier to take the step + over to production. If you want to do the same, update your + `app-config.production.yaml` as follows: + + ```diff + backend: + listen: + port: 7007 + + host: 0.0.0.0 + ``` + + Also, updated the builtin backend Dockerfile to honor the + `app-config.production.yaml` file. If you want to do the same, change + `packages/backend/Dockerfile` as follows: + + ```diff + -COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./ + +COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ + RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + + -CMD ["node", "packages/backend", "--config", "app-config.yaml"] + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] + ``` + + If you look carefully, this adds a glob match on app-config files. For those + that try out the build flows locally, you also want to make sure that the docker + daemon does NOT pick up any local/private config files that might contain + secrets. You should therefore also update your local `.dockerignore` file at the + same time: + + ```diff + +*.local.yaml + ``` + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. + +- 344ea56acc: Bump `commander` to version 9.1.0 + +- 806427545f: Added a link to the `${GITHUB_TOKEN}` to document how to generate a token + +## @backstage/dev-utils@1.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/test-utils@1.0.2-next.0 + +## @techdocs/cli@1.1.1-next.0 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## @backstage/techdocs-common@0.11.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## @backstage/test-utils@1.0.2-next.0 + +### Patch Changes + +- 7a5ddfd595: Added missing `Routes` element to wrap the `Route` elements of the test app wrapping. +- 7a5ddfd595: The internal elements created as part of the `mountedRoutes` implementation are now hidden during rendering. + +## @backstage/plugin-airbrake@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/test-utils@1.0.2-next.0 + - @backstage/dev-utils@1.0.2-next.0 + +## @backstage/plugin-airbrake-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-allure@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-api-docs@0.8.5-next.0 + +### Patch Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute +- 09fa05e5d7: Bumped @asyncapi/react-component > openapi-sampler > json-pointer to resolve security vulnerability CVE-2021-23820 +- 433a609a15: Updated the rendering of AsyncApi definitions to be able to resolve absolute HTTP \\$ref references. +- Updated dependencies + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-app-backend@0.3.32-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/config-loader@1.1.1-next.0 + +## @backstage/plugin-auth-backend@0.13.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. + +- 787ae0d541: Add more common predefined sign-in resolvers to auth providers. + + Add the existing resolver to more providers (already available at `google`): + + - `providers.microsoft.resolvers.emailLocalPartMatchingUserEntityName()` + - `providers.okta.resolvers.emailLocalPartMatchingUserEntityName()` + + Add a new resolver for simple email-to-email matching: + + - `providers.google.resolvers.emailMatchingUserEntityProfileEmail()` + - `providers.microsoft.resolvers.emailMatchingUserEntityProfileEmail()` + - `providers.okta.resolvers.emailMatchingUserEntityProfileEmail()` + +- 9ec4e0613e: Update to `jose` 4.6.0 + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## @backstage/plugin-auth-node@0.2.1-next.0 + +### Patch Changes + +- 9ec4e0613e: Update to `jose` 4.6.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-azure-devops@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-azure-devops-backend@0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-badges@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-badges-backend@0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-bazaar@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-bazaar-backend@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/backend-test-utils@0.1.24-next.0 + +## @backstage/plugin-bitrise@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-catalog-backend@1.1.2-next.0 + +### Patch Changes + +- 55e09b29dd: Fixing broken types for `knex` when checking returned rows +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.1.5-next.0 + +### Patch Changes + +- bffec1c96a: Fix S3 object URL creation at AwsS3EntityProvider by + + - handle absence of region config, + - handle regions with region-less URIs (us-east-1), + - apply URI encoding, + - and simplify the logic overall. + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.3.2-next.0 + +### Patch Changes + +- 8d9f673106: Add annotation `microsoft.com/email` when using the `defaultUserTransformer`. + + This will allow users of the Microsoft auth provider to utilize the predefined + SignIn resolver instead of maintaining their own. + + ```typescript + // backend/plugins/auth.ts + + // [...] + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + // [...] + providerFactories: { + microsoft: providers.microsoft.create({ + signIn: { + resolver: + providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); + } + ``` + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## @backstage/plugin-catalog-graph@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-catalog-import@0.8.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## @backstage/plugin-cicd-statistics@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.7-next.0 + +## @backstage/plugin-circleci@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-cloudbuild@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-code-climate@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-code-coverage@0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-code-coverage-backend@0.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-explore@0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-firehydrant@0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-fossa@0.2.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-gcalendar@0.3.1-next.0 + +### Patch Changes + +- 1429dbbea9: fixed google API types dependencies + +## @backstage/plugin-git-release-manager@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-github-actions@0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-github-deployments@0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## @backstage/plugin-gocd@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-graphql-backend@0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-home@0.4.21-next.0 + +### Patch Changes + +- 2b733d9d39: star icons now have the same yellow color as the other star icons when a entity is favourite +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/plugin-stack-overflow@0.1.1-next.0 + +## @backstage/plugin-ilert@0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-jenkins@0.7.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-jenkins-backend@0.1.22-next.0 + +### Patch Changes + +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## @backstage/plugin-kafka@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-kafka-backend@0.2.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-kubernetes@0.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-kubernetes-backend@0.5.1-next.0 + +### Patch Changes + +- b9f7ffb162: Add filtering to GKE cluster locator +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-lighthouse@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-newrelic-dashboard@0.1.13-next.0 + +### Patch Changes + +- 5fa2fb2553: Add `FetchApi` and related `fetchApiRef` which implement fetch. in order to included the Authorization Token to New Relic Dashboard Plugin, so that frontend can send an authenticated proxy call to backend +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-org@0.5.5-next.0 + +### Patch Changes + +- 8da4a207dd: Fix ref to filtered catalog table view when using aggregated relations. +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-pagerduty@0.3.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-periskop@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-periskop-backend@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-permission-backend@0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + +## @backstage/plugin-permission-node@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## @backstage/plugin-proxy-backend@0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-rollbar@0.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-rollbar-backend@0.1.29-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.7-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + +## @backstage/plugin-search@0.8.1-next.0 + +### Patch Changes + +- 11a46863de: Fix issue with `HomePageSearchBar` requiring `SearchContext` +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + +## @backstage/plugin-search-backend@0.5.2-next.0 + +### Patch Changes + +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@0.1.4-next.0 + +### Patch Changes + +- a7f7a63d14: Prevent orphaned stale indices by permanently marking them for deletion so removal can be re-attempted if it failed previously +- Updated dependencies + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## @backstage/plugin-search-backend-module-pg@0.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## @backstage/plugin-search-backend-node@0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.1-next.0 + +## @backstage/plugin-search-react@0.1.1-next.0 + +### Patch Changes + +- 11a46863de: Export `useSearchContextCheck` hook to check if the search context is available +- a307a14be0: Removed dependency on `@backstage/core-app-api`. + +## @backstage/plugin-sentry@0.3.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-sonarqube@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-splunk-on-call@0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-stack-overflow@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.21-next.0 + +## @backstage/plugin-tech-insights@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-tech-insights-backend@0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + +## @backstage/plugin-tech-insights-node@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## @backstage/plugin-techdocs@1.1.1-next.0 + +### Patch Changes + +- a307a14be0: Removed dependency on `@backstage/core-app-api`. +- bed0d64ce9: Fixed bugs that prevented a 404 error from being shown when it should have been. +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + +## @backstage/plugin-techdocs-backend@1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## @backstage/plugin-techdocs-node@1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- b26f4cb6a1: Allow usage of custom tags with sequence node type (such as the !ENV tag) in the techdocs mkdocs config. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + +## @backstage/plugin-techdocs-react@0.1.1-next.0 + +### Patch Changes + +- 075a9a067b: Updated the return type of `createTechDocsAddonExtension` to better reflect the fact that passing children to Addon components is not a valid use-case. + +## @backstage/plugin-todo@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## @backstage/plugin-todo-backend@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + +## example-app@0.2.71-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.2.0-next.0 + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-home@0.4.21-next.0 + - @backstage/plugin-api-docs@0.8.5-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-newrelic-dashboard@0.1.13-next.0 + - @backstage/plugin-search@0.8.1-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + - @backstage/plugin-techdocs@1.1.1-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@0.1.0-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + - @backstage/plugin-gcalendar@0.3.1-next.0 + - @backstage/plugin-org@0.5.5-next.0 + - @backstage/plugin-catalog-import@0.8.8-next.0 + - @backstage/plugin-github-actions@0.5.5-next.0 + - @backstage/plugin-airbrake@0.3.5-next.0 + - @backstage/plugin-azure-devops@0.1.21-next.0 + - @backstage/plugin-badges@0.2.29-next.0 + - @backstage/plugin-catalog-graph@0.2.17-next.0 + - @backstage/plugin-circleci@0.3.5-next.0 + - @backstage/plugin-cloudbuild@0.3.5-next.0 + - @backstage/plugin-code-coverage@0.1.32-next.0 + - @backstage/plugin-explore@0.3.36-next.0 + - @backstage/plugin-gocd@0.1.11-next.0 + - @backstage/plugin-jenkins@0.7.4-next.0 + - @backstage/plugin-kafka@0.3.5-next.0 + - @backstage/plugin-kubernetes@0.6.5-next.0 + - @backstage/plugin-lighthouse@0.3.5-next.0 + - @backstage/plugin-pagerduty@0.3.32-next.0 + - @backstage/plugin-rollbar@0.4.5-next.0 + - @backstage/plugin-sentry@0.3.43-next.0 + - @backstage/plugin-stack-overflow@0.1.1-next.0 + - @backstage/plugin-tech-insights@0.2.1-next.0 + - @backstage/plugin-todo@0.2.7-next.0 + +## example-backend@0.2.71-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/plugin-kubernetes-backend@0.5.1-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/plugin-app-backend@0.3.32-next.0 + - @backstage/plugin-auth-backend@0.13.1-next.0 + - @backstage/plugin-rollbar-backend@0.1.29-next.0 + - @backstage/plugin-techdocs-backend@1.1.1-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.4-next.0 + - @backstage/plugin-jenkins-backend@0.1.22-next.0 + - @backstage/plugin-search-backend@0.5.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - example-app@0.2.71-next.0 + - @backstage/plugin-azure-devops-backend@0.3.11-next.0 + - @backstage/plugin-badges-backend@0.1.26-next.0 + - @backstage/plugin-code-coverage-backend@0.1.30-next.0 + - @backstage/plugin-graphql-backend@0.1.22-next.0 + - @backstage/plugin-kafka-backend@0.2.25-next.0 + - @backstage/plugin-permission-backend@0.5.7-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + - @backstage/plugin-proxy-backend@0.2.26-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.7-next.0 + - @backstage/plugin-search-backend-module-pg@0.3.3-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + - @backstage/plugin-tech-insights-backend@0.3.1-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.16-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + - @backstage/plugin-todo-backend@0.1.29-next.0 + +## techdocs-cli-embedded-app@0.2.70-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-techdocs@1.1.1-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + - @backstage/test-utils@1.0.2-next.0 diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index 5d4268a4bc..c4e4bd83b6 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -55,3 +55,34 @@ launching Backstage, or remove the `${...}` values and simply set actual values directly for development. The Backstage App is now ready to start up with a PostgreSQL backing database. + +### Override default PostgreSQL Database Pool Configuration + +If you want to override the default connection pool settings then use the below configuration: + +```diff +backend: + database: +- client: better-sqlite3 +- connection: ':memory:' ++ # config options: https://node-postgres.com/api/client ++ client: pg ++ connection: ++ host: ${POSTGRES_HOST} ++ port: ${POSTGRES_PORT} ++ user: ${POSTGRES_USER} ++ password: ${POSTGRES_PASSWORD} ++ # Refer to Tarn docs for default values on PostgreSQL pool configuration - https://github.com/Vincit/tarn.js ++ knexConfig: ++ pool: ++ min: 3 ++ max: 12 ++ acquireTimeoutMillis: 60000 ++ idleTimeoutMillis: 60000 ++ # https://node-postgres.com/features/ssl ++ # you can set the sslmode configuration option via the `PGSSLMODE` environment variable ++ # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) ++ # ssl: ++ # ca: # if you have a CA file and want to verify it you can uncomment this section ++ # $file: /ca/server.crt +``` diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md index 771aa7a652..3002eb1aff 100644 --- a/docs/tutorials/using-backstage-proxy-within-plugin.md +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -52,7 +52,7 @@ calling `${backend-url}/api/proxy/`. The reason why `backend-url` is referenced is because the backstage backend creates and runs the proxy. Backstage is structured in such a way that you could run the backstage frontend independently of the backend. So when calling your API you -need to prepend the backend url to your http call. +need to prepend the backend URL to your http call. The recommended pattern for calling out to services is to wrap your calls in a [Utility API](../api/utility-apis.md). This section describes the steps to wrap diff --git a/microsite/data/plugins/adr.yaml b/microsite/data/plugins/adr.yaml new file mode 100644 index 0000000000..f180c80e36 --- /dev/null +++ b/microsite/data/plugins/adr.yaml @@ -0,0 +1,9 @@ +--- +title: Architecture Decision Records +author: Phil Kuang +authorUrl: https://github.com/kuangp +category: Discovery +description: Browse your project's ADRs. +documentation: https://github.com/backstage/backstage/tree/master/plugins/adr +iconUrl: img/adr-logo.png +npmPackageName: '@backstage/plugin-adr' diff --git a/microsite/data/plugins/cloud-carbon-footprint.yaml b/microsite/data/plugins/cloud-carbon-footprint.yaml new file mode 100644 index 0000000000..7e73ad6adb --- /dev/null +++ b/microsite/data/plugins/cloud-carbon-footprint.yaml @@ -0,0 +1,15 @@ +--- +title: Cloud Carbon Footprint +author: Thoughtworks +authorUrl: https://github.com/cloud-carbon-footprint +category: Metrics +description: View your cloud carbon footprint by estimating energy use (kilowatt-hours) and carbon emissions (metric tons CO2e) from public cloud usage. +documentation: https://github.com/cloud-carbon-footprint/ccf-backstage-plugin/blob/trunk/README.md +iconUrl: https://www.cloudcarbonfootprint.org/img/logo.png +npmPackageName: '@cloud-carbon-footprint/backstage-plugin-frontend' +tags: + - sustainability + - cloud + - climate + - carbon-emissions + - carbon-footprint diff --git a/microsite/package.json b/microsite/package.json index ebdc83dc23..c0c16d881b 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -16,7 +16,7 @@ "lock:check": "yarn-lock-check" }, "devDependencies": { - "@spotify/prettier-config": "^13.0.0", + "@spotify/prettier-config": "^13.0.1", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", "prettier": "^2.6.2", diff --git a/microsite/sidebars.json b/microsite/sidebars.json index d284a3c8ab..43e9dd3978 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,6 @@ { "releases": { - "Release Notes": ["releases/v1.0.0"] + "Release Notes": ["releases/v1.1.0", "releases/v1.0.0"] }, "docs": { "Overview": [ @@ -109,6 +109,7 @@ "features/techdocs/techdocs-overview", "features/techdocs/getting-started", "features/techdocs/concepts", + "features/techdocs/addons", "features/techdocs/architecture", "features/techdocs/creating-and-publishing", "features/techdocs/configuration", @@ -156,7 +157,10 @@ { "type": "subcategory", "label": "Gerrit", - "ids": ["integrations/gerrit/locations"] + "ids": [ + "integrations/gerrit/locations", + "integrations/gerrit/discovery" + ] }, { "type": "subcategory", @@ -164,7 +168,8 @@ "ids": [ "integrations/github/locations", "integrations/github/discovery", - "integrations/github/org" + "integrations/github/org", + "integrations/github/github-apps" ] }, { @@ -202,7 +207,6 @@ "plugins/proxying", "plugins/backend-plugin", "plugins/call-existing-api", - "plugins/github-apps", "plugins/url-reader" ] }, @@ -246,7 +250,6 @@ ] }, "auth/add-auth-provider", - "auth/using-auth", "auth/identity-resolver", "auth/auth-backend", "auth/oauth", diff --git a/microsite/static/img/adr-logo.png b/microsite/static/img/adr-logo.png new file mode 100644 index 0000000000..a71c54074d Binary files /dev/null and b/microsite/static/img/adr-logo.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 72d56b3521..448647da15 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -909,10 +909,10 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@spotify/prettier-config@^13.0.0": - version "13.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-13.0.0.tgz#ff6372cedf7e97e2e8a2d96c5ec12aa32cca3dbf" - integrity sha512-Hl3zgj67zaYejjLQLq0h0DlDMaI62EppO30AcStpLZNihjfwnFeS2+/gamiRHAnLki3P8bXaZVYwOV5NYNGQfw== +"@spotify/prettier-config@^13.0.1": + version "13.0.1" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-13.0.1.tgz#0fdceb3d4ab543259ce6adc0ec1d10e34898b812" + integrity sha512-oVd4hjx2+y0MeUdk1l+ItwVLwlrDlvTlGwXBWMMzPYc7DLyxuxFvDfoHGkAQkrikfAgtdnzxrW6u9a8ywUqdfw== "@types/cheerio@^0.22.8": version "0.22.23" @@ -1117,9 +1117,9 @@ async-each@^1.0.1: integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== async@^2.6.2: - version "2.6.3" - resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" diff --git a/mkdocs.yml b/mkdocs.yml index 204a4768c3..0e56dcbd73 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,7 @@ site_name: 'Backstage' site_description: 'Main documentation for Backstage features and platform APIs' +repo_url: https://github.com/backstage/backstage +edit_uri: edit/master/docs plugins: - techdocs-core @@ -99,10 +101,12 @@ nav: - Installation: 'integrations/datadog-rum/installation.md' - Gerrit: - Locations: 'integrations/gerrit/locations.md' + - Discovery: 'integrations/gerrit/discovery.md' - GitHub: - Locations: 'integrations/github/locations.md' - Discovery: 'integrations/github/discovery.md' - Org Data: 'integrations/github/org.md' + - GitHub Apps: 'integrations/github/github-apps.md' - GitLab: - Locations: 'integrations/gitlab/locations.md' - Discovery: 'integrations/gitlab/discovery.md' @@ -124,7 +128,6 @@ nav: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' - Call existing API: 'plugins/call-existing-api.md' - - GitHub Apps for Backend Authentication: 'plugins/github-apps.md' - URL Reader: 'plugins/url-reader.md' - Testing: - Testing with Jest: 'plugins/testing.md' @@ -150,7 +153,6 @@ nav: - OAuth2Proxy: 'auth/oauth2-proxy/provider.md' - Bitbucket: 'auth/bitbucket/provider.md' - Adding authentication providers: 'auth/add-auth-provider.md' - - Using authentication and identity: 'auth/using-auth.md' - Sign in resolvers: 'auth/identity-resolver.md' - Auth backend: 'auth/auth-backend.md' - OAuth and OpenID Connect: 'auth/oauth.md' diff --git a/package.json b/package.json index 6fea4e081a..db8816a7ac 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.1.0-next.2", + "version": "1.2.0-next.0", "dependencies": { "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.17.5", @@ -67,7 +67,7 @@ "cross-env": "^7.0.0", "concurrently": "^7.0.0", "eslint-plugin-notice": "^0.9.10", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "husky": "^7.0.4", "lerna": "^4.0.0", "lint-staged": "^12.2.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index d0b482dac7..cf09d57fcd 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/app-defaults +## 1.0.1 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 1.0.1-next.2 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-permission-react@0.4.0-next.1 + ## 1.0.1-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 0d70d29c92..ae18b4217f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.0.1-next.1", + "version": "1.0.1", "private": false, "publishConfig": { "access": "public", @@ -33,10 +33,10 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-permission-react": "^0.4.0-next.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-permission-react": "^0.4.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@types/jest": "^26.0.7", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 9fe57d36fc..498cdf6ed5 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,156 @@ # example-app +## 0.2.71-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.2.0-next.0 + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-home@0.4.21-next.0 + - @backstage/plugin-api-docs@0.8.5-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-newrelic-dashboard@0.1.13-next.0 + - @backstage/plugin-search@0.8.1-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + - @backstage/plugin-techdocs@1.1.1-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@0.1.0-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + - @backstage/plugin-gcalendar@0.3.1-next.0 + - @backstage/plugin-org@0.5.5-next.0 + - @backstage/plugin-catalog-import@0.8.8-next.0 + - @backstage/plugin-github-actions@0.5.5-next.0 + - @backstage/plugin-airbrake@0.3.5-next.0 + - @backstage/plugin-azure-devops@0.1.21-next.0 + - @backstage/plugin-badges@0.2.29-next.0 + - @backstage/plugin-catalog-graph@0.2.17-next.0 + - @backstage/plugin-circleci@0.3.5-next.0 + - @backstage/plugin-cloudbuild@0.3.5-next.0 + - @backstage/plugin-code-coverage@0.1.32-next.0 + - @backstage/plugin-explore@0.3.36-next.0 + - @backstage/plugin-gocd@0.1.11-next.0 + - @backstage/plugin-jenkins@0.7.4-next.0 + - @backstage/plugin-kafka@0.3.5-next.0 + - @backstage/plugin-kubernetes@0.6.5-next.0 + - @backstage/plugin-lighthouse@0.3.5-next.0 + - @backstage/plugin-pagerduty@0.3.32-next.0 + - @backstage/plugin-rollbar@0.4.5-next.0 + - @backstage/plugin-sentry@0.3.43-next.0 + - @backstage/plugin-stack-overflow@0.1.1-next.0 + - @backstage/plugin-tech-insights@0.2.1-next.0 + - @backstage/plugin-todo@0.2.7-next.0 + +## 0.2.70 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-home@0.4.20 + - @backstage/plugin-techdocs@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/cli@0.17.0 + - @backstage/plugin-scaffolder@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-kubernetes@0.6.4 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog-graph@0.2.16 + - @backstage/plugin-catalog-import@0.8.7 + - @backstage/plugin-kafka@0.3.4 + - @backstage/plugin-lighthouse@0.3.4 + - @backstage/plugin-rollbar@0.4.4 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-search@0.8.0 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-airbrake@0.3.4 + - @backstage/plugin-apache-airflow@0.1.12 + - @backstage/plugin-api-docs@0.8.4 + - @backstage/plugin-azure-devops@0.1.20 + - @backstage/plugin-badges@0.2.28 + - @backstage/plugin-catalog@1.1.0 + - @backstage/plugin-circleci@0.3.4 + - @backstage/plugin-cloudbuild@0.3.4 + - @backstage/plugin-code-coverage@0.1.31 + - @backstage/plugin-cost-insights@0.11.26 + - @backstage/plugin-explore@0.3.35 + - @backstage/plugin-gcalendar@0.3.0 + - @backstage/plugin-gcp-projects@0.3.23 + - @backstage/plugin-github-actions@0.5.4 + - @backstage/plugin-gocd@0.1.10 + - @backstage/plugin-graphiql@0.2.36 + - @backstage/plugin-jenkins@0.7.3 + - @backstage/plugin-newrelic@0.3.22 + - @backstage/plugin-org@0.5.4 + - @backstage/plugin-pagerduty@0.3.31 + - @backstage/plugin-sentry@0.3.42 + - @backstage/plugin-shortcuts@0.2.5 + - @backstage/plugin-stack-overflow@0.1.0 + - @backstage/plugin-tech-insights@0.2.0 + - @backstage/plugin-tech-radar@0.5.11 + - @backstage/plugin-todo@0.2.6 + - @backstage/plugin-user-settings@0.4.3 + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/app-defaults@1.0.1 + - @backstage/plugin-newrelic-dashboard@0.1.12 + +## 0.2.70-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.20-next.3 + - @backstage/plugin-techdocs@1.1.0-next.3 + - @backstage/cli@0.17.0-next.3 + - @backstage/plugin-scaffolder@1.1.0-next.3 + - @backstage/plugin-kubernetes@0.6.4-next.2 + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-airbrake@0.3.4-next.1 + - @backstage/plugin-apache-airflow@0.1.12-next.1 + - @backstage/plugin-api-docs@0.8.4-next.3 + - @backstage/plugin-azure-devops@0.1.20-next.1 + - @backstage/plugin-badges@0.2.28-next.1 + - @backstage/plugin-catalog-graph@0.2.16-next.3 + - @backstage/plugin-catalog-import@0.8.7-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-catalog@1.1.0-next.3 + - @backstage/plugin-circleci@0.3.4-next.1 + - @backstage/plugin-cloudbuild@0.3.4-next.1 + - @backstage/plugin-code-coverage@0.1.31-next.1 + - @backstage/plugin-cost-insights@0.11.26-next.1 + - @backstage/plugin-explore@0.3.35-next.1 + - @backstage/plugin-gcalendar@0.3.0-next.2 + - @backstage/plugin-gcp-projects@0.3.23-next.1 + - @backstage/plugin-github-actions@0.5.4-next.2 + - @backstage/plugin-gocd@0.1.10-next.2 + - @backstage/plugin-graphiql@0.2.36-next.2 + - @backstage/plugin-jenkins@0.7.3-next.1 + - @backstage/plugin-kafka@0.3.4-next.1 + - @backstage/plugin-lighthouse@0.3.4-next.1 + - @backstage/plugin-newrelic@0.3.22-next.1 + - @backstage/plugin-org@0.5.4-next.3 + - @backstage/plugin-pagerduty@0.3.31-next.1 + - @backstage/plugin-rollbar@0.4.4-next.1 + - @backstage/plugin-search@0.7.5-next.1 + - @backstage/plugin-sentry@0.3.42-next.2 + - @backstage/plugin-shortcuts@0.2.5-next.1 + - @backstage/plugin-stack-overflow@0.1.0-next.1 + - @backstage/plugin-tech-insights@0.2.0-next.1 + - @backstage/plugin-tech-radar@0.5.11-next.2 + - @backstage/plugin-todo@0.2.6-next.1 + - @backstage/plugin-user-settings@0.4.3-next.1 + - @backstage/app-defaults@1.0.1-next.2 + - @backstage/plugin-search-react@0.1.0-next.0 + - @backstage/plugin-permission-react@0.4.0-next.1 + - @backstage/plugin-newrelic-dashboard@0.1.12-next.1 + ## 0.2.70-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index f8dc6127c8..6e98358a75 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,63 +1,65 @@ { "name": "example-app", - "version": "0.2.70-next.1", + "version": "0.2.71-next.0", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^1.0.1-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/app-defaults": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", "@backstage/config": "^1.0.0", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-airbrake": "^0.3.4-next.0", - "@backstage/plugin-api-docs": "^0.8.4-next.1", - "@backstage/plugin-azure-devops": "^0.1.20-next.0", - "@backstage/plugin-apache-airflow": "^0.1.12-next.0", - "@backstage/plugin-badges": "^0.2.28-next.0", - "@backstage/plugin-catalog": "^1.1.0-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-catalog-graph": "^0.2.16-next.1", - "@backstage/plugin-catalog-import": "^0.8.7-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/plugin-circleci": "^0.3.4-next.0", - "@backstage/plugin-cloudbuild": "^0.3.4-next.0", - "@backstage/plugin-code-coverage": "^0.1.31-next.0", - "@backstage/plugin-cost-insights": "^0.11.26-next.0", - "@backstage/plugin-explore": "^0.3.35-next.0", - "@backstage/plugin-gcalendar": "^0.3.0-next.1", - "@backstage/plugin-gcp-projects": "^0.3.23-next.0", - "@backstage/plugin-github-actions": "^0.5.4-next.1", - "@backstage/plugin-gocd": "^0.1.10-next.0", - "@backstage/plugin-graphiql": "^0.2.36-next.0", - "@backstage/plugin-home": "^0.4.20-next.1", - "@backstage/plugin-jenkins": "^0.7.3-next.0", - "@backstage/plugin-kafka": "^0.3.4-next.0", - "@backstage/plugin-kubernetes": "^0.6.4-next.1", - "@backstage/plugin-lighthouse": "^0.3.4-next.0", - "@backstage/plugin-newrelic": "^0.3.22-next.0", - "@backstage/plugin-newrelic-dashboard": "^0.1.12-next.0", - "@backstage/plugin-org": "^0.5.4-next.1", - "@backstage/plugin-pagerduty": "0.3.31-next.0", - "@backstage/plugin-permission-react": "^0.4.0-next.0", - "@backstage/plugin-rollbar": "^0.4.4-next.0", - "@backstage/plugin-scaffolder": "^1.0.1-next.1", - "@backstage/plugin-search": "^0.7.5-next.0", - "@backstage/plugin-search-react": "^0.0.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", - "@backstage/plugin-sentry": "^0.3.42-next.0", - "@backstage/plugin-shortcuts": "^0.2.5-next.0", - "@backstage/plugin-stack-overflow": "^0.1.0-next.0", - "@backstage/plugin-tech-radar": "^0.5.11-next.1", - "@backstage/plugin-techdocs": "^1.0.1-next.1", - "@backstage/plugin-todo": "^0.2.6-next.0", - "@backstage/plugin-user-settings": "^0.4.3-next.0", - "@backstage/plugin-tech-insights": "^0.1.14-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-airbrake": "^0.3.5-next.0", + "@backstage/plugin-api-docs": "^0.8.5-next.0", + "@backstage/plugin-azure-devops": "^0.1.21-next.0", + "@backstage/plugin-apache-airflow": "^0.1.12", + "@backstage/plugin-badges": "^0.2.29-next.0", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-catalog-graph": "^0.2.17-next.0", + "@backstage/plugin-catalog-import": "^0.8.8-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-circleci": "^0.3.5-next.0", + "@backstage/plugin-cloudbuild": "^0.3.5-next.0", + "@backstage/plugin-code-coverage": "^0.1.32-next.0", + "@backstage/plugin-cost-insights": "^0.11.26", + "@backstage/plugin-explore": "^0.3.36-next.0", + "@backstage/plugin-gcalendar": "^0.3.1-next.0", + "@backstage/plugin-gcp-projects": "^0.3.23", + "@backstage/plugin-github-actions": "^0.5.5-next.0", + "@backstage/plugin-gocd": "^0.1.11-next.0", + "@backstage/plugin-graphiql": "^0.2.36", + "@backstage/plugin-home": "^0.4.21-next.0", + "@backstage/plugin-jenkins": "^0.7.4-next.0", + "@backstage/plugin-kafka": "^0.3.5-next.0", + "@backstage/plugin-kubernetes": "^0.6.5-next.0", + "@backstage/plugin-lighthouse": "^0.3.5-next.0", + "@backstage/plugin-newrelic": "^0.3.22", + "@backstage/plugin-newrelic-dashboard": "^0.1.13-next.0", + "@backstage/plugin-org": "^0.5.5-next.0", + "@backstage/plugin-pagerduty": "0.3.32-next.0", + "@backstage/plugin-permission-react": "^0.4.0", + "@backstage/plugin-rollbar": "^0.4.5-next.0", + "@backstage/plugin-scaffolder": "^1.2.0-next.0", + "@backstage/plugin-search": "^0.8.1-next.0", + "@backstage/plugin-search-react": "^0.1.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", + "@backstage/plugin-sentry": "^0.3.43-next.0", + "@backstage/plugin-shortcuts": "^0.2.5", + "@backstage/plugin-stack-overflow": "^0.1.1-next.0", + "@backstage/plugin-tech-radar": "^0.5.11", + "@backstage/plugin-techdocs": "^1.1.1-next.0", + "@backstage/plugin-techdocs-react": "^0.1.1-next.0", + "@backstage/plugin-techdocs-module-addons-contrib": "^0.1.0-next.0", + "@backstage/plugin-todo": "^0.2.7-next.0", + "@backstage/plugin-user-settings": "^0.4.3", + "@backstage/plugin-tech-insights": "^0.2.1-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -78,8 +80,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/plugin-permission-react": "^0.4.0-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/test-utils": "^1.0.2-next.0", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9cbbcc86a9..4cf2c02e55 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -66,9 +66,11 @@ import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechDocsIndexPage, - techdocsPlugin, TechDocsReaderPage, + techdocsPlugin, } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { UserSettingsPage, UserSettingsTab, @@ -87,6 +89,7 @@ import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTe import { searchPage } from './components/search/SearchPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; + import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; @@ -178,6 +181,9 @@ const routes = ( element={} > {techDocsPage} + + +
) => ( } to="/search"> - + + {({ toggleModal }) => } + }> diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index 6c8867ac28..2cd5481c91 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -22,6 +22,7 @@ import { WelcomeTitle, HeaderWorldClock, ClockConfig, + HomePageStarredEntities, } from '@backstage/plugin-home'; import { Content, Header, Page } from '@backstage/core-components'; import { HomePageSearchBar } from '@backstage/plugin-search'; @@ -104,6 +105,9 @@ export const homePage = ( + + + diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx new file mode 100644 index 0000000000..f75017a4dd --- /dev/null +++ b/packages/app/src/components/search/SearchModal.tsx @@ -0,0 +1,237 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + DialogActions, + DialogContent, + DialogTitle, + Grid, + List, + makeStyles, + Paper, + useTheme, +} from '@material-ui/core'; +import LaunchIcon from '@material-ui/icons/Launch'; +import { Link, useContent } from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + CATALOG_FILTER_EXISTS, +} from '@backstage/plugin-catalog-react'; +import { + DefaultResultListItem, + SearchBar, + SearchFilter, + searchPlugin, + SearchResult, + SearchResultPager, + SearchType, +} from '@backstage/plugin-search'; +import { useSearch } from '@backstage/plugin-search-react'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, + filters: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + }, + input: { + flex: 1, + }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, + viewResultsLink: { verticalAlign: '0.5em' }, +})); + +export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { + const getSearchLink = useRouteRef(searchPlugin.routes.root); + const classes = useStyles(); + + const catalogApi = useApi(catalogApiRef); + const { term, types } = useSearch(); + const { focusContent } = useContent(); + const { transitions } = useTheme(); + + const handleResultClick = () => { + toggleModal(); + setTimeout(focusContent, transitions.duration.leavingScreen); + }; + + const handleKeyPress = () => { + handleResultClick(); + }; + + return ( + <> + + + + + + + + + + + + {types.includes('techdocs') && ( + + { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); + + const names = items.map(entity => entity.metadata.name); + names.sort(); + return names; + }} + /> + + )} + + + + + + + + + { + toggleModal(); + setTimeout( + focusContent, + transitions.duration.leavingScreen, + ); + }} + to={`${getSearchLink()}?query=${term}`} + > + + View Full Results + + + + + + + + + {({ results }) => ( + + {results.map(({ type, document }) => { + let resultItem; + switch (type) { + case 'software-catalog': + resultItem = ( + + ); + break; + case 'techdocs': + resultItem = ( + + ); + break; + default: + resultItem = ( + + ); + } + return ( +
+ {resultItem} +
+ ); + })} +
+ )} +
+
+
+
+ + + + + + + + + ); +}; diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 65da72d91c..fd9c80686c 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -36,8 +36,8 @@ import { SearchResult, SearchResultPager, SearchType, - useSearch, } from '@backstage/plugin-search'; +import { useSearch } from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; import React, { useContext } from 'react'; diff --git a/packages/app/src/components/techdocs/TechDocsPage.tsx b/packages/app/src/components/techdocs/TechDocsPage.tsx index 4f49cf38e8..d79acbe0a7 100644 --- a/packages/app/src/components/techdocs/TechDocsPage.tsx +++ b/packages/app/src/components/techdocs/TechDocsPage.tsx @@ -14,30 +14,21 @@ * limitations under the License. */ -import { Content } from '@backstage/core-components'; +import React from 'react'; +import { Page } from '@backstage/core-components'; import { TechDocsReaderPageHeader, - TechDocsReaderPage, - Reader, + TechDocsReaderPageSubheader, + TechDocsReaderPageContent, } from '@backstage/plugin-techdocs'; -import React from 'react'; const DefaultTechDocsPage = () => { return ( - - {({ techdocsMetadataValue, entityMetadataValue, entityRef, onReady }) => ( - <> - - - - - - )} - + + + + + ); }; diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index e8adcd9181..d64b199d6c 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/backend-common +## 0.13.3-next.0 + +### Patch Changes + +- 6673babab9: Gerrit integration: Added optional `cloneUrl` string to config. +- 75bf9e1da9: Split BitbucketUrlReader into BitbucketCloudUrlReader and BitbucketServerUrlReader. Backwards compatible. +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9ec4e0613e: Update to `jose` 4.6.0 +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/config-loader@1.1.1-next.0 + +## 0.13.2 + +### Patch Changes + +- 19f6c6c32a: The backend will no longer fail to start up when configured secrets do not match the configuration schema. +- b7436743cb: Added the GerritUrlReader that implements "readUrl". +- 3ef123bbf0: Support external ID when assuming roles in S3 integration + + In order to assume a role created by a 3rd party as external + ID is needed. This change adds an optional field to the s3 + integration configuration and consumes that in the AwsS3UrlReader. + +- bae9359032: The logger returned from `getVoidLogger` is now uses a silenced console transport instead. +- 3ff0e79654: Tweaked the `UrlReader` multiplexer so that it uses the more helpful `NotAllowedError` messaging for all methods. +- 12608f8ba8: Add `@types/webpack-env` to dependencies. +- f9f512559b: Updated the visibility of database connection fields in config to be secret +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/config-loader@1.1.0 + +## 0.13.2-next.2 + +### Patch Changes + +- 19f6c6c32a: The backend will no longer fail to start up when configured secrets do not match the configuration schema. +- Updated dependencies + - @backstage/config-loader@1.1.0-next.1 + - @backstage/integration@1.1.0-next.2 + ## 0.13.2-next.1 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index cd4d08df87..961973b144 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -10,7 +10,9 @@ import { AbortController as AbortController_2 } from 'node-abort-controller'; import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; import { AwsS3Integration } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; +import { BitbucketCloudIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; +import { BitbucketServerIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; @@ -83,9 +85,54 @@ export class AzureUrlReader implements UrlReader { } // @public +export class BitbucketCloudUrlReader implements UrlReader { + constructor( + integration: BitbucketCloudIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + readUrl(url: string, options?: ReadUrlOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; +} + +// @public +export class BitbucketServerUrlReader implements UrlReader { + constructor( + integration: BitbucketServerIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); + // (undocumented) + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(url: string, options?: ReadTreeOptions): Promise; + // (undocumented) + readUrl(url: string, options?: ReadUrlOptions): Promise; + // (undocumented) + search(url: string, options?: SearchOptions): Promise; + // (undocumented) + toString(): string; +} + +// @public @deprecated export class BitbucketUrlReader implements UrlReader { constructor( integration: BitbucketIntegration, + logger: Logger, deps: { treeResponseFactory: ReadTreeResponseFactory; }, @@ -175,7 +222,7 @@ export class Contexts { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, -): Knex[]>; +): Knex; // @public export function createRootLogger( @@ -255,13 +302,19 @@ export type FromReadableArrayOptions = Array<{ // @public export class GerritUrlReader implements UrlReader { - constructor(integration: GerritIntegration); + constructor( + integration: GerritIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + workDir: string, + ); // (undocumented) static factory: ReaderFactory; // (undocumented) read(url: string): Promise; // (undocumented) - readTree(): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; // (undocumented) readUrl(url: string, options?: ReadUrlOptions): Promise; // (undocumented) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index bc2b25e4f4..d66465a471 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.13.2-next.1", + "version": "0.13.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -36,9 +36,9 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "@backstage/config": "^1.0.0", - "@backstage/config-loader": "^1.0.1-next.0", + "@backstage/config-loader": "^1.1.1-next.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", "@backstage/types": "^1.0.0", "@google-cloud/storage": "^5.8.0", "@manypkg/get-packages": "^1.1.3", @@ -47,6 +47,7 @@ "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/luxon": "^2.0.4", + "@types/webpack-env": "^1.15.2", "archiver": "^5.0.2", "aws-sdk": "^2.840.0", "compression": "^1.7.4", @@ -55,11 +56,11 @@ "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "git-url-parse": "^11.6.0", "helmet": "^5.0.2", "isomorphic-git": "^1.8.0", - "jose": "^1.27.1", + "jose": "^4.6.0", "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", "@keyv/redis": "^2.2.3", @@ -89,8 +90,8 @@ } }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^2.0.0", diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index c6cad2e49b..82c04f2cdd 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -18,7 +18,6 @@ import { ConfigReader } from '@backstage/config'; import Keyv from 'keyv'; /* @ts-expect-error */ import KeyvMemcache from 'keyv-memcache'; -/* @ts-expect-error */ import KeyvRedis from '@keyv/redis'; import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; @@ -215,7 +214,7 @@ describe('CacheManager', () => { ttl: expectedTtl, }); expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); - const redis = KeyvRedis as jest.Mock; + const redis = KeyvRedis as unknown as jest.Mock; const mockRedisCalls = redis.mock.calls.splice(-1); expect(mockRedisCalls[0][0]).toEqual(redisConnection); }); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index d8652e42d8..99e3770ada 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -18,7 +18,6 @@ import { Config } from '@backstage/config'; import Keyv from 'keyv'; // @ts-expect-error import KeyvMemcache from 'keyv-memcache'; -// @ts-expect-error import KeyvRedis from '@keyv/redis'; import { Logger } from 'winston'; import { getRootLogger } from '../logging'; diff --git a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts new file mode 100644 index 0000000000..38841d75ad --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts @@ -0,0 +1,362 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { + BitbucketCloudIntegration, + readBitbucketCloudIntegrationConfig, +} from '@backstage/integration'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import os from 'os'; +import path from 'path'; +import { NotModifiedError } from '@backstage/errors'; +import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; +import { DefaultReadTreeResponseFactory } from './tree'; + +const treeResponseFactory = DefaultReadTreeResponseFactory.create({ + config: new ConfigReader({}), +}); + +const reader = new BitbucketCloudUrlReader( + new BitbucketCloudIntegration( + readBitbucketCloudIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + username: 'username', + appPassword: 'password', + }), + ), + ), + { treeResponseFactory }, +); + +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + +describe('BitbucketCloudUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const worker = setupServer(); + setupRequestMockHandlers(worker); + + describe('readUrl', () => { + it('should be able to readUrl without ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBeNull(); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'etag-value'), + ); + }, + ), + ); + + const result = await reader.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + ); + const buffer = await result.buffer(); + expect(buffer.toString()).toBe('foo'); + }); + + it('should be able to readUrl with matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'matching-etag-value', + ); + return res(ctx.status(304)); + }, + ), + ); + + await expect( + reader.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'matching-etag-value' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + + it('should be able to readUrl without matching ETag', async () => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', + (req, res, ctx) => { + expect(req.headers.get('If-None-Match')).toBe( + 'previous-etag-value', + ); + return res( + ctx.status(200), + ctx.body('foo'), + ctx.set('ETag', 'new-etag-value'), + ); + }, + ), + ); + + const result = await reader.readUrl( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + { etag: 'previous-etag-value' }, + ); + const buffer = await result.buffer(); + expect(buffer.toString()).toBe('foo'); + expect(result.etag).toBe('new-etag-value'); + }); + }); + + describe('read', () => { + it('rejects unknown targets', async () => { + await expect( + reader.read('https://not.bitbucket.com/apa'), + ).rejects.toThrow( + 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket Cloud URL or file path', + ); + }); + }); + + describe('readTree', () => { + const repoBuffer = fs.readFileSync( + path.resolve( + __dirname, + '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', + ), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + mainbranch: { + type: 'branch', + name: 'master', + }, + }), + ), + ), + rest.get( + 'https://bitbucket.org/backstage/mock/get/master.tar.gz', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('returns the wanted files from an archive', async () => { + const response = await reader.readTree( + 'https://bitbucket.org/backstage/mock/src/master', + ); + + expect(response.etag).toBe('12ab34cd56ef'); + + const files = await response.files(); + + expect(files.length).toBe(2); + const mkDocsFile = await files[0].content(); + const indexMarkdownFile = await files[1].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + expect(mkDocsFile.toString()).toBe('site_name: Test\n'); + }); + + it('creates a directory with the wanted files', async () => { + const response = await reader.readTree( + 'https://bitbucket.org/backstage/mock', + ); + + const dir = await response.dir({ targetDir: tmpDir }); + + await expect( + fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), + ).resolves.toBe('site_name: Test\n'); + await expect( + fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + + it('returns the wanted files from an archive with a subpath', async () => { + const response = await reader.readTree( + 'https://bitbucket.org/backstage/mock/src/master/docs', + ); + + expect(response.etag).toBe('12ab34cd56ef'); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('creates a directory with the wanted files with a subpath', async () => { + const response = await reader.readTree( + 'https://bitbucket.org/backstage/mock/src/master/docs', + ); + + const dir = await response.dir({ targetDir: tmpDir }); + + await expect( + fs.readFile(path.join(dir, 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + + it('throws a NotModifiedError when given a etag in options', async () => { + const fnBitbucket = async () => { + await reader.readTree('https://bitbucket.org/backstage/mock', { + etag: '12ab34cd56ef', + }); + }; + + await expect(fnBitbucket).rejects.toThrow(NotModifiedError); + }); + + it('should not throw a NotModifiedError when given an outdated etag in options', async () => { + const response = await reader.readTree( + 'https://bitbucket.org/backstage/mock', + { etag: 'outdatedetag123abc' }, + ); + + expect(response.etag).toBe('12ab34cd56ef'); + }); + }); + + describe('search hosted', () => { + const repoBuffer = fs.readFileSync( + path.resolve( + __dirname, + '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', + ), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + mainbranch: { + type: 'branch', + name: 'master', + }, + }), + ), + ), + rest.get( + 'https://bitbucket.org/backstage/mock/get/master.tar.gz', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await reader.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('works in nested folders', async () => { + const result = await reader.search( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.*', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + reader.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); +}); diff --git a/packages/backend-common/src/reading/BitbucketCloudUrlReader.ts b/packages/backend-common/src/reading/BitbucketCloudUrlReader.ts new file mode 100644 index 0000000000..6a9406e920 --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketCloudUrlReader.ts @@ -0,0 +1,234 @@ +/* + * 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 { NotFoundError, NotModifiedError } from '@backstage/errors'; +import { + BitbucketCloudIntegration, + getBitbucketCloudDefaultBranch, + getBitbucketCloudDownloadUrl, + getBitbucketCloudFileFetchUrl, + getBitbucketCloudRequestOptions, + ScmIntegrations, +} from '@backstage/integration'; +import fetch, { Response } from 'node-fetch'; +import parseGitUrl from 'git-url-parse'; +import { trimEnd } from 'lodash'; +import { Minimatch } from 'minimatch'; +import { Readable } from 'stream'; +import { + ReaderFactory, + ReadTreeOptions, + ReadTreeResponse, + ReadTreeResponseFactory, + ReadUrlOptions, + ReadUrlResponse, + SearchOptions, + SearchResponse, + UrlReader, +} from './types'; + +/** + * Implements a {@link UrlReader} for files from Bitbucket Cloud. + * + * @public + */ +export class BitbucketCloudUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { + const integrations = ScmIntegrations.fromConfig(config); + return integrations.bitbucketCloud.list().map(integration => { + const reader = new BitbucketCloudUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; + return { reader, predicate }; + }); + }; + + constructor( + private readonly integration: BitbucketCloudIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) { + const { host, username, appPassword } = integration.config; + + if (username && !appPassword) { + throw new Error( + `Bitbucket Cloud integration for '${host}' has configured a username but is missing a required appPassword.`, + ); + } + } + + async read(url: string): Promise { + const response = await this.readUrl(url); + return response.buffer(); + } + + async readUrl( + url: string, + options?: ReadUrlOptions, + ): Promise { + const { etag, signal } = options ?? {}; + const bitbucketUrl = getBitbucketCloudFileFetchUrl( + url, + this.integration.config, + ); + const requestOptions = getBitbucketCloudRequestOptions( + this.integration.config, + ); + + let response: Response; + try { + response = await fetch(bitbucketUrl.toString(), { + headers: { + ...requestOptions.headers, + ...(etag && { 'If-None-Match': etag }), + }, + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can be + // removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + ...(signal && { signal: signal as any }), + }); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.status === 304) { + throw new NotModifiedError(); + } + + if (response.ok) { + return { + buffer: async () => Buffer.from(await response.arrayBuffer()), + etag: response.headers.get('ETag') ?? undefined, + }; + } + + const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const { filepath } = parseGitUrl(url); + + const lastCommitShortHash = await this.getLastCommitShortHash(url); + if (options?.etag && options.etag === lastCommitShortHash) { + throw new NotModifiedError(); + } + + const downloadUrl = await getBitbucketCloudDownloadUrl( + url, + this.integration.config, + ); + const archiveResponse = await fetch( + downloadUrl, + getBitbucketCloudRequestOptions(this.integration.config), + ); + if (!archiveResponse.ok) { + const message = `Failed to read tree from ${url}, ${archiveResponse.status} ${archiveResponse.statusText}`; + if (archiveResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + return await this.deps.treeResponseFactory.fromTarArchive({ + stream: archiveResponse.body as unknown as Readable, + subpath: filepath, + etag: lastCommitShortHash, + filter: options?.filter, + }); + } + + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = trimEnd(url.replace(filepath, ''), '/'); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(path), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; + } + + toString() { + const { host, username, appPassword } = this.integration.config; + const authed = Boolean(username && appPassword); + return `bitbucketCloud{host=${host},authed=${authed}}`; + } + + private async getLastCommitShortHash(url: string): Promise { + const { name: repoName, owner: project, ref } = parseGitUrl(url); + + let branch = ref; + if (!branch) { + branch = await getBitbucketCloudDefaultBranch( + url, + this.integration.config, + ); + } + + const commitsApiUrl = `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`; + + const commitsResponse = await fetch( + commitsApiUrl, + getBitbucketCloudRequestOptions(this.integration.config), + ); + if (!commitsResponse.ok) { + const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; + if (commitsResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const commits = await commitsResponse.json(); + if ( + commits && + commits.values && + commits.values.length > 0 && + commits.values[0].hash + ) { + return commits.values[0].hash.substring(0, 12); + } + + throw new Error(`Failed to read response from ${commitsApiUrl}`); + } +} diff --git a/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts new file mode 100644 index 0000000000..1417752494 --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts @@ -0,0 +1,184 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { + BitbucketServerIntegration, + readBitbucketServerIntegrationConfig, +} from '@backstage/integration'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import os from 'os'; +import path from 'path'; +import { NotModifiedError } from '@backstage/errors'; +import { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; +import { DefaultReadTreeResponseFactory } from './tree'; + +const treeResponseFactory = DefaultReadTreeResponseFactory.create({ + config: new ConfigReader({}), +}); + +const reader = new BitbucketServerUrlReader( + new BitbucketServerIntegration( + readBitbucketServerIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }), + ), + ), + { treeResponseFactory }, +); + +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + +describe('BitbucketServerUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const worker = setupServer(); + setupRequestMockHandlers(worker); + + describe('readTree', () => { + const repoBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock.tgz', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('uses private bitbucket host', async () => { + const response = await reader.readTree( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', + ); + + expect(response.etag).toBe('12ab34cd56ef'); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + }); + + describe('search private', () => { + const repoBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock.tgz', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await reader.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('works in nested folders', async () => { + const result = await reader.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + reader.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); +}); diff --git a/packages/backend-common/src/reading/BitbucketServerUrlReader.ts b/packages/backend-common/src/reading/BitbucketServerUrlReader.ts new file mode 100644 index 0000000000..320b8f2719 --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketServerUrlReader.ts @@ -0,0 +1,218 @@ +/* + * 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 { NotFoundError, NotModifiedError } from '@backstage/errors'; +import { + BitbucketServerIntegration, + getBitbucketServerDownloadUrl, + getBitbucketServerFileFetchUrl, + getBitbucketServerRequestOptions, + ScmIntegrations, +} from '@backstage/integration'; +import fetch, { Response } from 'node-fetch'; +import parseGitUrl from 'git-url-parse'; +import { trimEnd } from 'lodash'; +import { Minimatch } from 'minimatch'; +import { Readable } from 'stream'; +import { + ReaderFactory, + ReadTreeOptions, + ReadTreeResponse, + ReadTreeResponseFactory, + ReadUrlOptions, + ReadUrlResponse, + SearchOptions, + SearchResponse, + UrlReader, +} from './types'; + +/** + * Implements a {@link UrlReader} for files from Bitbucket Server APIs. + * + * @public + */ +export class BitbucketServerUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { + const integrations = ScmIntegrations.fromConfig(config); + return integrations.bitbucketServer.list().map(integration => { + const reader = new BitbucketServerUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; + return { reader, predicate }; + }); + }; + + constructor( + private readonly integration: BitbucketServerIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) {} + + async read(url: string): Promise { + const response = await this.readUrl(url); + return response.buffer(); + } + + async readUrl( + url: string, + options?: ReadUrlOptions, + ): Promise { + const { etag, signal } = options ?? {}; + const bitbucketUrl = getBitbucketServerFileFetchUrl( + url, + this.integration.config, + ); + const requestOptions = getBitbucketServerRequestOptions( + this.integration.config, + ); + + let response: Response; + try { + response = await fetch(bitbucketUrl.toString(), { + headers: { + ...requestOptions.headers, + ...(etag && { 'If-None-Match': etag }), + }, + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can be + // removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + ...(signal && { signal: signal as any }), + }); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.status === 304) { + throw new NotModifiedError(); + } + + if (response.ok) { + return { + buffer: async () => Buffer.from(await response.arrayBuffer()), + etag: response.headers.get('ETag') ?? undefined, + }; + } + + const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const { filepath } = parseGitUrl(url); + + const lastCommitShortHash = await this.getLastCommitShortHash(url); + if (options?.etag && options.etag === lastCommitShortHash) { + throw new NotModifiedError(); + } + + const downloadUrl = await getBitbucketServerDownloadUrl( + url, + this.integration.config, + ); + const archiveResponse = await fetch( + downloadUrl, + getBitbucketServerRequestOptions(this.integration.config), + ); + if (!archiveResponse.ok) { + const message = `Failed to read tree from ${url}, ${archiveResponse.status} ${archiveResponse.statusText}`; + if (archiveResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + return await this.deps.treeResponseFactory.fromTarArchive({ + stream: archiveResponse.body as unknown as Readable, + subpath: filepath, + etag: lastCommitShortHash, + filter: options?.filter, + }); + } + + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = trimEnd(url.replace(filepath, ''), '/'); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(path), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; + } + + toString() { + const { host, token } = this.integration.config; + const authed = Boolean(token); + return `bitbucketServer{host=${host},authed=${authed}}`; + } + + private async getLastCommitShortHash(url: string): Promise { + const { name: repoName, owner: project } = parseGitUrl(url); + + // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 + const commitsApiUrl = `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; + + const commitsResponse = await fetch( + commitsApiUrl, + getBitbucketServerRequestOptions(this.integration.config), + ); + if (!commitsResponse.ok) { + const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; + if (commitsResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const commits = await commitsResponse.json(); + if ( + commits && + commits.values && + commits.values.length > 0 && + commits.values[0].id + ) { + return commits.values[0].id.substring(0, 12); + } + + throw new Error(`Failed to read response from ${commitsApiUrl}`); + } +} diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index eff982581b..3a2360debb 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -29,38 +29,76 @@ import path from 'path'; import { NotModifiedError } from '@backstage/errors'; import { BitbucketUrlReader } from './BitbucketUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +import { getVoidLogger } from '../logging'; -const treeResponseFactory = DefaultReadTreeResponseFactory.create({ - config: new ConfigReader({}), +const logger = getVoidLogger(); + +describe('BitbucketUrlReader.factory', () => { + it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => { + const config = new ConfigReader({ + integrations: { + bitbucket: [], + bitbucketCloud: [ + { + username: 'username', + appPassword: 'password', + }, + ], + bitbucketServer: [ + { + host: 'bitbucket-server.local', + token: 'test-token', + }, + ], + }, + }); + const treeResponseFactory = DefaultReadTreeResponseFactory.create({ + config: config, + }); + + const tuples = BitbucketUrlReader.factory({ + config, + logger, + treeResponseFactory, + }); + + expect(tuples).toHaveLength(0); + }); }); -const bitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }), - ), - ), - { treeResponseFactory }, -); - -const hostedBitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }), - ), - ), - { treeResponseFactory }, -); - -const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; - describe('BitbucketUrlReader', () => { + const treeResponseFactory = DefaultReadTreeResponseFactory.create({ + config: new ConfigReader({}), + }); + + const bitbucketProcessor = new BitbucketUrlReader( + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }), + ), + ), + logger, + { treeResponseFactory }, + ); + + const hostedBitbucketProcessor = new BitbucketUrlReader( + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }), + ), + ), + logger, + { treeResponseFactory }, + ); + + const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + beforeEach(() => { mockFs({ [tmpDir]: mockFs.directory(), diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index df22f1633f..1687a66262 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -28,6 +28,7 @@ import parseGitUrl from 'git-url-parse'; import { trimEnd } from 'lodash'; import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; +import { Logger } from 'winston'; import { ReaderFactory, ReadTreeOptions, @@ -45,24 +46,38 @@ import { * as the one exposed by Bitbucket Cloud itself. * * @public + * @deprecated in favor of BitbucketCloudUrlReader and BitbucketServerUrlReader */ export class BitbucketUrlReader implements UrlReader { - static factory: ReaderFactory = ({ config, treeResponseFactory }) => { + static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); - return integrations.bitbucket.list().map(integration => { - const reader = new BitbucketUrlReader(integration, { - treeResponseFactory, + return integrations.bitbucket + .list() + .filter( + item => + !integrations.bitbucketCloud.byHost(item.config.host) && + !integrations.bitbucketServer.byHost(item.config.host), + ) + .map(integration => { + const reader = new BitbucketUrlReader(integration, logger, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; + return { reader, predicate }; }); - const predicate = (url: URL) => url.host === integration.config.host; - return { reader, predicate }; - }); }; constructor( private readonly integration: BitbucketIntegration, + logger: Logger, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { const { host, token, username, appPassword } = integration.config; + const replacement = + host === 'bitbucket.org' ? 'bitbucketCloud' : 'bitbucketServer'; + logger.warn( + `[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`, + ); if (!token && username && !appPassword) { throw new Error( diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts index bc88fdcb20..fc27b3dd6a 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.test.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts @@ -14,24 +14,36 @@ * limitations under the License. */ -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { getVoidLogger } from '../logging'; -import { DefaultReadTreeResponseFactory } from './tree'; -import { UrlReaderPredicateTuple } from './types'; +import { NotModifiedError, NotFoundError } from '@backstage/errors'; import { GerritIntegration, readGerritIntegrationConfig, } from '@backstage/integration'; +import { JsonObject } from '@backstage/types'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import mockFs from 'mock-fs'; +import fs from 'fs-extra'; +import path from 'path'; +import { getVoidLogger } from '../logging'; +import { UrlReaderPredicateTuple } from './types'; +import { DefaultReadTreeResponseFactory } from './tree'; import { GerritUrlReader } from './GerritUrlReader'; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +jest.mock('../scm', () => ({ + Git: { + fromAuth: () => ({ + clone: jest.fn(() => Promise.resolve({})), + }), + }, +})); + const gerritProcessor = new GerritUrlReader( new GerritIntegration( readGerritIntegrationConfig( @@ -40,6 +52,8 @@ const gerritProcessor = new GerritUrlReader( }), ), ), + { treeResponseFactory }, + '/tmp', ); const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { @@ -54,6 +68,10 @@ describe('GerritUrlReader', () => { const worker = setupServer(); setupRequestMockHandlers(worker); + afterAll(() => { + jest.clearAllMocks(); + }); + describe('reader factory', () => { it('creates a reader.', () => { const readers = createReader({ @@ -103,7 +121,7 @@ describe('GerritUrlReader', () => { expect(predicate(new URL('https://gerrit-review.com/path'))).toBe(false); }); - it('returns false for host.', () => { + it('returns false for host.', () => { expect(predicate(new URL('https://gerrit.com/path'))).toBe(true); }); }); @@ -170,4 +188,104 @@ describe('GerritUrlReader', () => { ); }); }); + + describe('readTree', () => { + const branchAPIUrl = + 'https://gerrit.com/projects/app%2Fweb/branches/master'; + const branchAPIresponse = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/gerrit/branch-info-response.txt'), + ); + const treeUrl = 'https://gerrit.com/app/web/+/refs/heads/master/'; + const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1'; + const mkdocsContent = 'great content'; + const mdContent = 'doc'; + + beforeEach(() => { + mockFs({ + '/tmp/': mockFs.directory(), + '/tmp/gerrit-clone-123abc/repo/mkdocs.yml': mkdocsContent, + '/tmp/gerrit-clone-123abc/repo/docs/first.md': mdContent, + }); + const spy = jest.spyOn(fs, 'mkdtemp'); + spy.mockImplementation(() => '/tmp/gerrit-clone-123abc'); + }); + + afterEach(() => { + mockFs.restore(); + jest.clearAllMocks(); + }); + + it('reads the wanted files correctly.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessor.readTree(treeUrl); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(2); + + const docsYaml = await files[0].content(); + expect(docsYaml.toString()).toBe(mkdocsContent); + + const mdFile = await files[1].content(); + expect(mdFile.toString()).toBe(mdContent); + }); + + it('throws NotModifiedError for matching etags.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl, { etag })).rejects.toThrow( + NotModifiedError, + ); + }); + + it('throws NotFoundError if branch info not found.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(404, 'Not found.')); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow( + NotFoundError, + ); + }); + + it('should throw on failures while getting branch info.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(500, 'Error')); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(Error); + }); + + it('should returns wanted files with a subpath', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessor.readTree(`${treeUrl}/docs`); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(1); + + const mdFile = await files[0].content(); + expect(mdFile.toString()).toBe(mdContent); + }); + }); }); diff --git a/packages/backend-common/src/reading/GerritUrlReader.ts b/packages/backend-common/src/reading/GerritUrlReader.ts index 7cc0b4731b..f3e8924192 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.ts @@ -14,16 +14,30 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/errors'; +import { Git } from '../scm'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { GerritIntegration, + getGerritCloneRepoUrl, + getGerritBranchApiUrl, getGerritFileContentsApiUrl, getGerritRequestOptions, + parseGerritJsonResponse, + parseGerritGitilesUrl, } from '@backstage/integration'; +import concatStream from 'concat-stream'; +import fs from 'fs-extra'; import fetch, { Response } from 'node-fetch'; +import os from 'os'; +import { join as joinPath } from 'path'; +import tar from 'tar'; +import { pipeline as pipelineCb, Readable } from 'stream'; +import { promisify } from 'util'; import { ReaderFactory, + ReadTreeOptions, ReadTreeResponse, + ReadTreeResponseFactory, ReadUrlOptions, ReadUrlResponse, SearchResponse, @@ -31,6 +45,11 @@ import { } from './types'; import { ScmIntegrations } from '@backstage/integration'; +const pipeline = promisify(pipelineCb); + +const createTemporaryDirectory = async (workDir: string): Promise => + await fs.mkdtemp(joinPath(workDir, '/gerrit-clone-')); + /** * Implements a {@link UrlReader} for files in Gerrit. * @@ -50,16 +69,22 @@ import { ScmIntegrations } from '@backstage/integration'; * @public */ export class GerritUrlReader implements UrlReader { - static factory: ReaderFactory = ({ config }) => { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); if (!integrations.gerrit) { return []; } + const workDir = + config.getOptionalString('backend.workingDirectory') ?? os.tmpdir(); return integrations.gerrit.list().map(integration => { - const reader = new GerritUrlReader(integration); + const reader = new GerritUrlReader( + integration, + { treeResponseFactory }, + workDir, + ); const predicate = (url: URL) => { const gitilesUrl = new URL(integration.config.gitilesBaseUrl!); - // If gitilesUrl is not specfified it will default to + // If gitilesUrl is not specified it will default to // "integration.config.host". return url.host === gitilesUrl.host; }; @@ -67,7 +92,11 @@ export class GerritUrlReader implements UrlReader { }); }; - constructor(private readonly integration: GerritIntegration) {} + constructor( + private readonly integration: GerritIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly workDir: string, + ) {} async read(url: string): Promise { const response = await this.readUrl(url); @@ -109,8 +138,72 @@ export class GerritUrlReader implements UrlReader { ); } - async readTree(): Promise { - throw new Error('GerritReader does not implement readTree'); + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const { filePath } = parseGerritGitilesUrl(this.integration.config, url); + const apiUrl = getGerritBranchApiUrl(this.integration.config, url); + let response: Response; + try { + response = await fetch(apiUrl, { + method: 'GET', + ...getGerritRequestOptions(this.integration.config), + }); + } catch (e) { + throw new Error(`Unable to read branch state ${url}, ${e}`); + } + + if (response.status === 404) { + throw new NotFoundError(`Not found: ${url}`); + } + + if (!response.ok) { + throw new Error( + `${url} could not be read as ${apiUrl}, ${response.status} ${response.statusText}`, + ); + } + const branchInfo = (await parseGerritJsonResponse(response as any)) as { + revision: string; + }; + if (options?.etag === branchInfo.revision) { + throw new NotModifiedError(); + } + + const git = Git.fromAuth({ + username: this.integration.config.username, + password: this.integration.config.password, + }); + const tempDir = await createTemporaryDirectory(this.workDir); + const cloneUrl = getGerritCloneRepoUrl(this.integration.config, url); + try { + // The "fromTarArchive" function will strip the top level directory so + // an additional directory level is created when we clone. + await git.clone({ + url: cloneUrl, + dir: joinPath(tempDir, 'repo'), + ref: branchInfo.revision, + depth: 1, + }); + + const data = await new Promise(async resolve => { + await pipeline( + tar.create({ cwd: tempDir }, ['']), + concatStream(resolve), + ); + }); + const tarArchive = Readable.from(data); + return await this.deps.treeResponseFactory.fromTarArchive({ + stream: tarArchive as unknown as Readable, + subpath: filePath === '/' ? undefined : filePath, + etag: branchInfo.revision, + filter: options?.filter, + }); + } catch (error) { + throw new Error(`Could not clone ${cloneUrl}: ${error}`); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } } async search(): Promise { diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts new file mode 100644 index 0000000000..527f26a9ec --- /dev/null +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { getVoidLogger } from '../logging'; +import { UrlReaderPredicateMux } from './UrlReaderPredicateMux'; + +describe('UrlReaderPredicateMux', () => { + it('forwards methods based on predicate', async () => { + const fooReader = { + read: jest.fn(), + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + const barReader = { + read: jest.fn(), + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const mux = new UrlReaderPredicateMux(getVoidLogger()); + mux.register({ + predicate: url => url.hostname === 'foo', + reader: fooReader, + }); + mux.register({ + predicate: url => url.hostname === 'bar', + reader: barReader, + }); + + await mux.read('http://foo/1'); + expect(fooReader.read).toHaveBeenCalledWith('http://foo/1'); + await mux.readUrl('http://foo/2'); + expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/2', undefined); + await mux.readTree('http://foo/3'); + expect(fooReader.readTree).toHaveBeenCalledWith('http://foo/3', undefined); + await mux.search('http://foo/4'); + expect(fooReader.search).toHaveBeenCalledWith('http://foo/4', undefined); + + await mux.read('http://bar/1'); + expect(barReader.read).toHaveBeenCalledWith('http://bar/1'); + await mux.readUrl('http://bar/2'); + expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/2', undefined); + await mux.readTree('http://bar/3'); + expect(barReader.readTree).toHaveBeenCalledWith('http://bar/3', undefined); + await mux.search('http://bar/4'); + expect(barReader.search).toHaveBeenCalledWith('http://bar/4', undefined); + }); + + it('throws an error if no predicate matches', async () => { + const mux = new UrlReaderPredicateMux(getVoidLogger()); + + await expect(mux.readUrl('http://foo/1')).rejects.toThrowError( + /^Reading from 'http:\/\/foo\/1' is not allowed. You may/, + ); + + mux.register({ + predicate: url => url.hostname === 'foo', + reader: { + read: jest.fn(), + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }, + }); + + await expect(mux.readUrl('http://foo/1')).resolves.toBeUndefined(); + + await expect(mux.readUrl('http://bar/1')).rejects.toThrowError( + /^Reading from 'http:\/\/bar\/1' is not allowed. You may/, + ); + }); +}); diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts index bbe94a0fb4..13d85be4d4 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -29,6 +29,14 @@ import { const MIN_WARNING_INTERVAL_MS = 1000 * 60 * 15; +function notAllowedMessage(url: string) { + return ( + `Reading from '${url}' is not allowed. ` + + `You may need to configure an integration for the target host, or add it ` + + `to the configured list of allowed hosts at 'backend.reading.allow'` + ); +} + /** * A UrlReader implementation that selects from a set of UrlReaders * based on a predicate tied to each reader. @@ -52,11 +60,7 @@ export class UrlReaderPredicateMux implements UrlReader { } } - throw new NotAllowedError( - `Reading from '${url}' is not allowed. ` + - `You may need to configure an integration for the target host, or add it ` + - `to the configured list of allowed hosts at 'backend.reading.allow'`, - ); + throw new NotAllowedError(notAllowedMessage(url)); } async readUrl( @@ -87,7 +91,7 @@ export class UrlReaderPredicateMux implements UrlReader { } } - throw new NotAllowedError(`Reading from '${url}' is not allowed`); + throw new NotAllowedError(notAllowedMessage(url)); } async readTree( @@ -102,7 +106,7 @@ export class UrlReaderPredicateMux implements UrlReader { } } - throw new NotAllowedError(`Reading from '${url}' is not allowed`); + throw new NotAllowedError(notAllowedMessage(url)); } async search(url: string, options?: SearchOptions): Promise { @@ -114,7 +118,7 @@ export class UrlReaderPredicateMux implements UrlReader { } } - throw new NotAllowedError(`Reading from '${url}' is not allowed`); + throw new NotAllowedError(notAllowedMessage(url)); } toString() { diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index e676d0e14c..72833454c3 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -19,6 +19,8 @@ import { Config } from '@backstage/config'; import { ReaderFactory, UrlReader } from './types'; import { UrlReaderPredicateMux } from './UrlReaderPredicateMux'; import { AzureUrlReader } from './AzureUrlReader'; +import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; +import { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; import { BitbucketUrlReader } from './BitbucketUrlReader'; import { GerritUrlReader } from './GerritUrlReader'; import { GithubUrlReader } from './GithubUrlReader'; @@ -82,6 +84,8 @@ export class UrlReaders { config, factories: factories.concat([ AzureUrlReader.factory, + BitbucketCloudUrlReader.factory, + BitbucketServerUrlReader.factory, BitbucketUrlReader.factory, GerritUrlReader.factory, GithubUrlReader.factory, diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt b/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt new file mode 100644 index 0000000000..20018c1bdf --- /dev/null +++ b/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt @@ -0,0 +1,2 @@ +)]}' +{"web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/app/web/+/refs/heads/master","target":"_blank"}],"ref":"refs/heads/master","revision":"52432507a70b677b5674b019c9a46b2e9f29d0a1"} diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index c2c82e2bd5..f1609179b5 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -15,7 +15,9 @@ */ export { AzureUrlReader } from './AzureUrlReader'; +export { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; export { BitbucketUrlReader } from './BitbucketUrlReader'; +export { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; export { GerritUrlReader } from './GerritUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts index 1799291544..0ff7bfb316 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.test.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger } from '../logging/voidLogger'; import { ConfigReader } from '@backstage/config'; import { ServerTokenManager } from './ServerTokenManager'; import { Logger } from 'winston'; -import { JWK } from 'jose'; +import * as jose from 'jose'; import { TokenManager } from './types'; const emptyConfig = new ConfigReader({}); @@ -204,7 +204,7 @@ describe('ServerTokenManager', () => { }); describe('NODE_ENV === development', () => { - const generateSyncSpy = jest.spyOn(JWK, 'generateSync'); + const generateSecretSpy = jest.spyOn(jose, 'generateSecret'); beforeEach(() => { (process.env as any).NODE_ENV = 'development'; @@ -214,26 +214,30 @@ describe('ServerTokenManager', () => { jest.clearAllMocks(); }); - it('should generate a key if no config is provided', () => { - ServerTokenManager.fromConfig(emptyConfig, { logger }); - - expect(generateSyncSpy).toHaveBeenCalledWith('oct', 192); + it('should generate a key if no config is provided', async () => { + const tokenManager = ServerTokenManager.fromConfig(emptyConfig, { + logger, + }); + const token = await tokenManager.getToken(); + expect(token).toBeDefined(); + expect(generateSecretSpy).toHaveBeenCalledWith('HS256'); }); - it('should generate a key if no keys are provided in the configuration', () => { - ServerTokenManager.fromConfig( + it('should generate a key if no keys are provided in the configuration', async () => { + const tokenManager = ServerTokenManager.fromConfig( new ConfigReader({ backend: { auth: { keys: [] } }, }), { logger }, ); - - expect(generateSyncSpy).toHaveBeenCalledWith('oct', 192); + const token = await tokenManager.getToken(); + expect(token).toBeDefined(); + expect(generateSecretSpy).toHaveBeenCalledWith('HS256'); }); it('should use provided secrets if config is provided', () => { ServerTokenManager.fromConfig(configWithSecret, { logger }); - expect(generateSyncSpy).not.toHaveBeenCalled(); + expect(generateSecretSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.ts b/packages/backend-common/src/tokens/ServerTokenManager.ts index d6693edba1..484b99ca4d 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JWKS, JWK, JWT } from 'jose'; +import { base64url, generateSecret, SignJWT, jwtVerify, exportJWK } from 'jose'; import { Config } from '@backstage/config'; import { AuthenticationError } from '@backstage/errors'; import { TokenManager } from './types'; @@ -37,8 +37,10 @@ class NoopTokenManager implements TokenManager { * @public */ export class ServerTokenManager implements TokenManager { - private readonly verificationKeys: JWKS.KeyStore; - private readonly signingKey: JWK.Key; + private verificationKeys: Uint8Array[]; + private signingKey: Uint8Array; + private privateKeyPromise?: Promise; + private logger: Logger; static noop(): TokenManager { return new NoopTokenManager(); @@ -49,51 +51,87 @@ export class ServerTokenManager implements TokenManager { const keys = config.getOptionalConfigArray('backend.auth.keys'); if (keys?.length) { - return new ServerTokenManager(keys.map(key => key.getString('secret'))); + return new ServerTokenManager( + keys.map(key => key.getString('secret')), + logger, + ); } if (process.env.NODE_ENV !== 'development') { throw new Error( 'You must configure at least one key in backend.auth.keys for production.', ); } - // For development, if a secret has not been configured, we auto generate a secret instead of throwing. - const generatedDevOnlyKey = JWK.generateSync('oct', 24 * 8); - if (generatedDevOnlyKey.k === undefined) { - throw new Error('Internal error, JWK key generation returned no data'); - } logger.warn( 'Generated a secret for backend-to-backend authentication: DEVELOPMENT USE ONLY.', ); - return new ServerTokenManager([generatedDevOnlyKey.k]); + return new ServerTokenManager([], logger); } - private constructor(secrets: string[]) { - if (!secrets.length) { + private constructor(secrets: string[], logger: Logger) { + if (!secrets.length && process.env.NODE_ENV !== 'development') { throw new Error( 'No secrets provided when constructing ServerTokenManager', ); } + this.logger = logger; + this.verificationKeys = secrets.map(s => base64url.decode(s)); + this.signingKey = this.verificationKeys[0]; + } - this.verificationKeys = new JWKS.KeyStore( - secrets.map(k => JWK.asKey({ kty: 'oct', k })), - ); - this.signingKey = this.verificationKeys.all()[0]; + // Called when no keys have been generated yet in the dev environment + private async generateKeys(): Promise { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Key generation is not supported outside of the dev environment', + ); + } + if (this.privateKeyPromise) { + return this.privateKeyPromise; + } + const promise = (async () => { + const secret = await generateSecret('HS256'); + const jwk = await exportJWK(secret); + this.verificationKeys.push(base64url.decode(jwk.k ?? '')); + this.signingKey = this.verificationKeys[0]; + return; + })(); + + try { + // If we fail to generate a new key, we need to clear the state so that + // the next caller will try to generate another key. + await promise; + } catch (error) { + this.logger.error(`Failed to generate new key, ${error}`); + delete this.privateKeyPromise; + } + return promise; } async getToken(): Promise<{ token: string }> { - const jwt = JWT.sign({ sub: 'backstage-server' }, this.signingKey, { - algorithm: 'HS256', - }); - + if (!this.verificationKeys.length) { + await this.generateKeys(); + } + const sub = 'backstage-server'; + const jwt = await new SignJWT({ alg: 'HS256' }) + .setProtectedHeader({ alg: 'HS256', sub: sub }) + .setSubject('backstage-server') + .sign(this.signingKey); return { token: jwt }; } async authenticate(token: string): Promise { - try { - JWT.verify(token, this.verificationKeys); - } catch (e) { - throw new AuthenticationError('Invalid server token'); + let verifyError = undefined; + for (const key of this.verificationKeys) { + try { + await jwtVerify(token, key); + // If the verify succeeded, return + return; + } catch (e) { + // Catch the verify exception and continue + verifyError = e; + } } + throw new AuthenticationError(`Invalid server token: ${verifyError}`); } } diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index ee63a45598..a8c66d866d 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/backend-tasks +## 0.3.1-next.0 + +### Patch Changes + +- cfd779a9bc: Scheduled tasks now have an optional `scope` field. If unset, or having the + value `'global'`, the old behavior with cross-worker locking is retained. If + having the value `'local'`, there is no coordination across workers and the + behavior is more like `setInterval`. This can be used to replace usages of + `runPeriodically` helpers. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.3.0 + +### Minor Changes + +- ab008a0988: Adds the ability to manually trigger tasks which are registered + +### Patch Changes + +- bdd2773202: Refactored the internal `TaskWorker` class to make it easier to test. +- a83babdd63: Fixed the `initialDelay` parameter of tasks to properly make task workers + _always_ wait before the first invocations on startup, not just the very first + time that the task is ever created. This behavior is more in line with + expectations. Callers to not need to update their code. + + Also clarified in the doc comment for the field that this wait applies only on + an individual worker level. That is, if you have a cluster of workers then each + individual machine may postpone its first task invocation by the given amount of + time to leave room for the service to settle, but _other_ workers may still + continue to invoke the task on the regular cadence in the meantime. + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.3.0-next.2 ### Patch Changes diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 9f02088bdf..0c82387cc8 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -43,6 +43,7 @@ export interface TaskScheduleDefinition { } | Duration; initialDelay?: Duration; + scope?: 'global' | 'local'; timeout: Duration; } diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 0456a34bff..20eab4e58e 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.3.0-next.2", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -33,7 +33,7 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@backstage/types": "^1.0.0", @@ -48,8 +48,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.2", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/cron": "^1.7.3", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts b/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts new file mode 100644 index 0000000000..bdbd976def --- /dev/null +++ b/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts @@ -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. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { AbortController } from 'node-abort-controller'; +import { LocalTaskWorker } from './LocalTaskWorker'; + +describe('LocalTaskWorker', () => { + const logger = getVoidLogger(); + + it('runs the happy path (with iso duration) and handles cancellation', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: 'PT0.2S', + timeoutAfterDuration: 'PT1S', + }, + { signal: controller.signal }, + ); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toBeCalledTimes(1); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toBeCalledTimes(2); + controller.abort(); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toBeCalledTimes(2); + }); + + it('runs the happy path (with a cron expression) and handles cancellation', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + // Await until system time is just past a second boundary (since cron is + // wall clock based) + await new Promise(r => setTimeout(r, 1000 - (Date.now() % 1000) + 10)); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: '* * * * * *', + timeoutAfterDuration: 'PT1S', + }, + { signal: controller.signal }, + ); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toBeCalledTimes(1); + await new Promise(r => setTimeout(r, 1000)); + expect(fn).toBeCalledTimes(2); + controller.abort(); + await new Promise(r => setTimeout(r, 1000)); + expect(fn).toBeCalledTimes(2); + }); + + it('can trigger to abort wait', async () => { + const fn = jest.fn(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start({ + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: 'PT0.2S', + timeoutAfterDuration: 'PT1S', + }); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toBeCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toBeCalledTimes(1); + worker.trigger(); + await new Promise(r => setTimeout(r, 10)); + expect(fn).toBeCalledTimes(2); + }); +}); diff --git a/packages/backend-tasks/src/tasks/LocalTaskWorker.ts b/packages/backend-tasks/src/tasks/LocalTaskWorker.ts new file mode 100644 index 0000000000..89f314a1ec --- /dev/null +++ b/packages/backend-tasks/src/tasks/LocalTaskWorker.ts @@ -0,0 +1,146 @@ +/* + * 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 { ConflictError } from '@backstage/errors'; +import { CronTime } from 'cron'; +import { DateTime, Duration } from 'luxon'; +import { AbortController, AbortSignal } from 'node-abort-controller'; +import { Logger } from 'winston'; +import { TaskFunction, TaskSettingsV2 } from './types'; +import { delegateAbortController, sleep } from './util'; + +/** + * Implements tasks that run locally without cross-host collaboration. + * + * @private + */ +export class LocalTaskWorker { + private abortWait: AbortController | undefined; + + constructor( + private readonly taskId: string, + private readonly fn: TaskFunction, + private readonly logger: Logger, + ) {} + + start(settings: TaskSettingsV2, options?: { signal?: AbortSignal }) { + this.logger.info( + `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`, + ); + + (async () => { + try { + if (settings.initialDelayDuration) { + await this.sleep( + Duration.fromISO(settings.initialDelayDuration), + options?.signal, + ); + } + + while (!options?.signal?.aborted) { + const startTime = process.hrtime(); + await this.runOnce(settings, options?.signal); + const timeTaken = process.hrtime(startTime); + await this.waitUntilNext( + settings, + (timeTaken[0] + timeTaken[1] / 1e9) * 1000, + options?.signal, + ); + } + + this.logger.info(`Task worker finished: ${this.taskId}`); + } catch (e) { + this.logger.warn(`Task worker failed unexpectedly, ${e}`); + } + })(); + } + + trigger(): void { + if (!this.abortWait) { + throw new ConflictError(`Task ${this.taskId} is currently running`); + } + this.abortWait.abort(); + } + + /** + * Makes a single attempt at running the task to completion. + */ + private async runOnce( + settings: TaskSettingsV2, + signal?: AbortSignal, + ): Promise { + // Abort the task execution either if the worker is stopped, or if the + // task timeout is hit + const taskAbortController = delegateAbortController(signal); + const timeoutHandle = setTimeout(() => { + taskAbortController.abort(); + }, Duration.fromISO(settings.timeoutAfterDuration).as('milliseconds')); + + try { + await this.fn(taskAbortController.signal); + } catch (e) { + // ignore intentionally + } + + // release resources + clearTimeout(timeoutHandle); + taskAbortController.abort(); + } + + /** + * Sleeps until it's time to run the task again. + */ + private async waitUntilNext( + settings: TaskSettingsV2, + lastRunMillis: number, + signal?: AbortSignal, + ) { + if (signal?.aborted) { + return; + } + + const isCron = !settings.cadence.startsWith('P'); + let dt: number; + + if (isCron) { + const nextRun = +new CronTime(settings.cadence).sendAt().toDate(); + dt = nextRun - Date.now(); + } else { + dt = + Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis; + } + + dt = Math.max(dt, 0); + + this.logger.debug( + `task: ${this.taskId} will next occur around ${DateTime.now().plus( + Duration.fromMillis(dt), + )}`, + ); + + await this.sleep(Duration.fromMillis(dt), signal); + } + + private async sleep( + duration: Duration, + abortSignal?: AbortSignal, + ): Promise { + this.abortWait = delegateAbortController(abortSignal); + await sleep(duration, this.abortWait.signal); + this.abortWait.abort(); // cleans up resources + this.abortWait = undefined; + } +} diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 004426211c..1204431434 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -16,11 +16,11 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { ConflictError, NotFoundError } from '@backstage/errors'; import { Duration } from 'luxon'; +import { AbortSignal } from 'node-abort-controller'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; -import { ConflictError, NotFoundError } from '@backstage/errors'; -import { AbortSignal } from 'node-abort-controller'; jest.useFakeTimers(); @@ -49,7 +49,7 @@ describe('PluginTaskManagerImpl', () => { // This is just to test the wrapper code; most of the actual tests are in // TaskWorker.test.ts - describe('scheduleTask', () => { + describe('scheduleTask with global scope', () => { it.each(databases.eachSupportedId())( 'can run the v1 happy path, %p', async databaseId => { @@ -62,6 +62,7 @@ describe('PluginTaskManagerImpl', () => { timeout: Duration.fromMillis(5000), frequency: Duration.fromMillis(5000), fn, + scope: 'global', }); await promise; @@ -82,6 +83,7 @@ describe('PluginTaskManagerImpl', () => { timeout: Duration.fromMillis(5000), frequency: { cron: '* * * * * *' }, fn, + scope: 'global', }); await promise; @@ -91,7 +93,7 @@ describe('PluginTaskManagerImpl', () => { ); }); - describe('triggerTask', () => { + describe('triggerTask with global scope', () => { it.each(databases.eachSupportedId())( 'can manually trigger a task, %p', async databaseId => { @@ -105,6 +107,7 @@ describe('PluginTaskManagerImpl', () => { frequency: Duration.fromObject({ years: 1 }), initialDelay: Duration.fromObject({ years: 1 }), fn, + scope: 'global', }); await manager.triggerTask('task1'); @@ -127,6 +130,7 @@ describe('PluginTaskManagerImpl', () => { timeout: Duration.fromMillis(5000), frequency: Duration.fromObject({ years: 1 }), fn, + scope: 'global', }); await expect(() => manager.triggerTask('task2')).rejects.toThrow( @@ -151,6 +155,7 @@ describe('PluginTaskManagerImpl', () => { resolve(); await new Promise(r => setTimeout(r, 20000)); }, + scope: 'global', }); await promise; @@ -162,6 +167,106 @@ describe('PluginTaskManagerImpl', () => { ); }); + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('scheduleTask with local scope', () => { + it('can run the v1 happy path', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + scope: 'local', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + + it('can run the v2 happy path', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task2', + timeout: Duration.fromMillis(5000), + frequency: { cron: '* * * * * *' }, + fn, + scope: 'local', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + }); + + describe('triggerTask with local scope', () => { + it('can manually trigger a task', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn, + scope: 'local', + }); + + await manager.triggerTask('task1'); + jest.advanceTimersByTime(5000); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + + it('cant trigger a non-existent task', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn, + scope: 'local', + }); + + await expect(() => manager.triggerTask('task2')).rejects.toThrow( + NotFoundError, + ); + }, 60_000); + + it('cant trigger a running task', async () => { + const { manager } = await init('SQLITE_3'); + + const { promise, resolve } = defer(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn: async () => { + resolve(); + await new Promise(r => setTimeout(r, 20000)); + }, + scope: 'local', + }); + + await promise; + await expect(() => manager.triggerTask('task1')).rejects.toThrow( + ConflictError, + ); + }, 60_000); + }); + // This is just to test the wrapper code; most of the actual tests are in // TaskWorker.test.ts describe('createScheduledTaskRunner', () => { @@ -176,6 +281,7 @@ describe('PluginTaskManagerImpl', () => { .createScheduledTaskRunner({ timeout: Duration.fromMillis(5000), frequency: Duration.fromMillis(5000), + scope: 'global', }) .run({ id: 'task1', diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 44f46e7237..6db8b9f4f5 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -16,6 +16,7 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; +import { LocalTaskWorker } from './LocalTaskWorker'; import { TaskWorker } from './TaskWorker'; import { PluginTaskScheduler, @@ -24,62 +25,73 @@ import { TaskScheduleDefinition, } from './types'; import { validateId } from './util'; -import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; -import { ConflictError, NotFoundError } from '@backstage/errors'; /** * Implements the actual task management. */ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { + private readonly localTasksById = new Map(); + constructor( private readonly databaseFactory: () => Promise, private readonly logger: Logger, ) {} async triggerTask(id: string): Promise { + const localTask = this.localTasksById.get(id); + if (localTask) { + localTask.trigger(); + return; + } + const knex = await this.databaseFactory(); - - // check if task exists - const rows = await knex(DB_TASKS_TABLE) - .select(knex.raw(1)) - .where('id', '=', id); - if (rows.length !== 1) { - throw new NotFoundError(`Task ${id} does not exist`); - } - - const updatedRows = await knex(DB_TASKS_TABLE) - .where('id', '=', id) - .whereNull('current_run_ticket') - .update({ - next_run_start_at: knex.fn.now(), - }); - if (updatedRows < 1) { - throw new ConflictError(`Task ${id} is currently running`); - } + await TaskWorker.trigger(knex, id); } async scheduleTask( task: TaskScheduleDefinition & TaskInvocationDefinition, ): Promise { validateId(task.id); + const scope = task.scope ?? 'global'; - const knex = await this.databaseFactory(); - const worker = new TaskWorker(task.id, task.fn, knex, this.logger); + if (scope === 'global') { + const knex = await this.databaseFactory(); + const worker = new TaskWorker(task.id, task.fn, knex, this.logger); - await worker.start( - { - version: 2, - cadence: - 'cron' in task.frequency - ? task.frequency.cron - : task.frequency.toISO(), - initialDelayDuration: task.initialDelay?.toISO(), - timeoutAfterDuration: task.timeout.toISO(), - }, - { - signal: task.signal, - }, - ); + await worker.start( + { + version: 2, + cadence: + 'cron' in task.frequency + ? task.frequency.cron + : task.frequency.toISO(), + initialDelayDuration: task.initialDelay?.toISO(), + timeoutAfterDuration: task.timeout.toISO(), + }, + { + signal: task.signal, + }, + ); + } else { + const worker = new LocalTaskWorker(task.id, task.fn, this.logger); + + worker.start( + { + version: 2, + cadence: + 'cron' in task.frequency + ? task.frequency.cron + : task.frequency.toISO(), + initialDelayDuration: task.initialDelay?.toISO(), + timeoutAfterDuration: task.timeout.toISO(), + }, + { + signal: task.signal, + }, + ); + + this.localTasksById.set(task.id, worker); + } } createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner { diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index 2a5deddb19..e59f24c277 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -17,8 +17,8 @@ import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; -import { TaskScheduler } from './TaskScheduler'; import waitForExpect from 'wait-for-expect'; +import { TaskScheduler } from './TaskScheduler'; describe('TaskScheduler', () => { const logger = getVoidLogger(); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 0c64d06287..14688d0f98 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { AbortController } from 'node-abort-controller'; import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabases } from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; +import { AbortController } from 'node-abort-controller'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 02bc115ddd..a60a0a4730 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ConflictError, NotFoundError } from '@backstage/errors'; +import { CronTime } from 'cron'; import { Knex } from 'knex'; import { DateTime, Duration } from 'luxon'; import { AbortSignal } from 'node-abort-controller'; @@ -22,12 +24,11 @@ import { Logger } from 'winston'; import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables'; import { TaskFunction, TaskSettingsV2, taskSettingsV2Schema } from './types'; import { delegateAbortController, nowPlus, sleep } from './util'; -import { CronTime } from 'cron'; const DEFAULT_WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); /** - * Performs the actual work of a task. + * Implements tasks that run across worker hosts, with collaborative locking. * * @private */ @@ -76,12 +77,32 @@ export class TaskWorker { })(); } + static async trigger(knex: Knex, taskId: string): Promise { + // check if task exists + const rows = await knex(DB_TASKS_TABLE) + .select(knex.raw(1)) + .where('id', '=', taskId); + if (rows.length !== 1) { + throw new NotFoundError(`Task ${taskId} does not exist`); + } + + const updatedRows = await knex(DB_TASKS_TABLE) + .where('id', '=', taskId) + .whereNull('current_run_ticket') + .update({ + next_run_start_at: knex.fn.now(), + }); + if (updatedRows < 1) { + throw new ConflictError(`Task ${taskId} is currently running`); + } + } + /** * Makes a single attempt at running the task to completion, if ready. * * @returns The outcome of the attempt */ - async runOnce( + private async runOnce( signal?: AbortSignal, ): Promise< | { result: 'not-ready-yet' } diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 1e09e52cd0..f9ada1dd17 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import { CronTime } from 'cron'; import { Duration } from 'luxon'; import { AbortSignal } from 'node-abort-controller'; import { z } from 'zod'; -import { CronTime } from 'cron'; /** * A function that can be called as a scheduled task. @@ -93,14 +93,37 @@ export interface TaskScheduleDefinition { * will happen as soon as possible according to the cadence. * * NOTE: This is a per-worker delay. If you have a cluster of workers all - * collaborating on a task, then you may still see the task being processed by - * other long-lived workers, while any given single worker is in its initial - * sleep delay time e.g. after a deployment. Therefore this parameter is not - * useful for "globally" pausing work; its main intended use is for individual - * machines to get a chance to reach some equilibrium at startup before - * triggering heavy batch workloads. + * collaborating on a task that has its `scope` field set to `'global'`, then + * you may still see the task being processed by other long-lived workers, + * while any given single worker is in its initial sleep delay time e.g. after + * a deployment. Therefore this parameter is not useful for "globally" pausing + * work; its main intended use is for individual machines to get a chance to + * reach some equilibrium at startup before triggering heavy batch workloads. */ initialDelay?: Duration; + + /** + * Sets the scope of concurrency control / locking to apply for invocations of + * this task. + * + * @remarks + * + * When the scope is set to the default value `'global'`, the scheduler will + * attempt to ensure that only one worker machine runs the task at a time, + * according to the given cadence. This means that as the number of worker + * hosts increases, the invocation frequency of this task will not go up. + * Instead the load is spread randomly across hosts. This setting is useful + * for tasks that access shared resources, for example catalog ingestion tasks + * where you do not want many machines to repeatedly import the same data and + * trample over each other. + * + * When the scope is set to `'local'`, there is no concurrency control across + * hosts. Each host runs the task according to the given cadence similarly to + * `setInterval`, but the runtime ensures that there are no overlapping runs. + * + * @defaultValue 'global' + */ + scope?: 'global' | 'local'; } /** @@ -149,24 +172,23 @@ export interface PluginTaskScheduler { /** * Manually triggers a task by ID. * - * If the task doesn't exist, a NotFoundError is thrown. - * If the task is currently running, a ConflictError is thrown. + * If the task doesn't exist, a NotFoundError is thrown. If the task is + * currently running, a ConflictError is thrown. * * @param id - The task ID - * */ triggerTask(id: string): Promise; /** - * Schedules a task function for coordinated exclusive invocation across - * workers. This convenience method performs both the scheduling and - * invocation in one go. + * Schedules a task function for recurring runs. * * @remarks * - * If the task was already scheduled since before by us or by another party, - * its options are just overwritten with the given options, and things - * continue from there. + * The `scope` task field controls whether to use coordinated exclusive + * invocation across workers, or to just coordinate within the current worker. + * + * This convenience method performs both the scheduling and invocation in one + * go. * * @param task - The task definition */ diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 79f07c4ff4..4a6a5a0c25 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/backend-test-utils +## 0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/cli@0.17.1-next.0 + +## 0.1.23 + +### Patch Changes + +- 0654c87cf2: `TestDatabases.create` will no longer set up an `afterAll` test handler if no databases are supported. +- Updated dependencies + - @backstage/cli@0.17.0 + - @backstage/backend-common@0.13.2 + ## 0.1.23-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index cb487f3e3f..eb6f7e0aba 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.23-next.1", + "version": "0.1.24-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -34,8 +34,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/cli": "^0.17.1-next.0", "@backstage/config": "^1.0.0", "better-sqlite3": "^7.5.0", "knex": "^1.0.2", @@ -46,7 +46,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index f5d01249a5..8a6a052a20 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,104 @@ # example-backend +## 0.2.71-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/plugin-kubernetes-backend@0.5.1-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/plugin-app-backend@0.3.32-next.0 + - @backstage/plugin-auth-backend@0.13.1-next.0 + - @backstage/plugin-rollbar-backend@0.1.29-next.0 + - @backstage/plugin-techdocs-backend@1.1.1-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.4-next.0 + - @backstage/plugin-jenkins-backend@0.1.22-next.0 + - @backstage/plugin-search-backend@0.5.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - example-app@0.2.71-next.0 + - @backstage/plugin-azure-devops-backend@0.3.11-next.0 + - @backstage/plugin-badges-backend@0.1.26-next.0 + - @backstage/plugin-code-coverage-backend@0.1.30-next.0 + - @backstage/plugin-graphql-backend@0.1.22-next.0 + - @backstage/plugin-kafka-backend@0.2.25-next.0 + - @backstage/plugin-permission-backend@0.5.7-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + - @backstage/plugin-proxy-backend@0.2.26-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.7-next.0 + - @backstage/plugin-search-backend-module-pg@0.3.3-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + - @backstage/plugin-tech-insights-backend@0.3.1-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.16-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + - @backstage/plugin-todo-backend@0.1.29-next.0 + +## 0.2.70 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/plugin-techdocs-backend@1.1.0 + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/plugin-search-backend@0.5.0 + - @backstage/plugin-auth-backend@0.13.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.15 + - @backstage/plugin-kafka-backend@0.2.24 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/plugin-jenkins-backend@0.1.20 + - @backstage/plugin-badges-backend@0.1.25 + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/plugin-todo-backend@0.1.28 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-kubernetes-backend@0.5.0 + - @backstage/plugin-search-backend-node@0.6.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.3 + - @backstage/plugin-search-backend-module-pg@0.3.2 + - @backstage/plugin-permission-backend@0.5.6 + - @backstage/plugin-tech-insights-backend@0.3.0 + - @backstage/plugin-azure-devops-backend@0.3.10 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.6 + - example-app@0.2.70 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-app-backend@0.3.31 + - @backstage/plugin-code-coverage-backend@0.1.29 + - @backstage/plugin-graphql-backend@0.1.21 + - @backstage/plugin-proxy-backend@0.2.25 + - @backstage/plugin-rollbar-backend@0.1.28 + +## 0.2.70-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.13.0-next.2 + - @backstage/plugin-catalog-backend@1.1.0-next.3 + - @backstage/plugin-kafka-backend@0.2.24-next.1 + - @backstage/plugin-search-backend@0.5.0-next.2 + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/plugin-permission-node@0.6.0-next.2 + - @backstage/plugin-jenkins-backend@0.1.20-next.2 + - @backstage/plugin-todo-backend@0.1.28-next.2 + - @backstage/backend-common@0.13.2-next.2 + - @backstage/plugin-kubernetes-backend@0.5.0-next.1 + - @backstage/plugin-search-backend-node@0.6.0-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.6-next.2 + - @backstage/integration@1.1.0-next.2 + - @backstage/plugin-techdocs-backend@1.1.0-next.2 + - example-app@0.2.70-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.3-next.1 + - @backstage/plugin-search-backend-module-pg@0.3.2-next.1 + - @backstage/plugin-app-backend@0.3.31-next.1 + ## 0.2.70-next.1 ### Patch Changes diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index dd35d4ddbc..86f6e17861 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -13,18 +13,18 @@ FROM node:16-bullseye-slim WORKDIR /app +# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. # The skeleton contains the package.json of each package in the monorepo, # and along with yarn.lock and the root package.json, that's enough to run yarn install. COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -# install sqlite3 dependencies -RUN apt-get update && \ - apt-get install -y libsqlite3-dev python3 cmake g++ && \ - rm -rf /var/lib/apt/lists/* && \ - yarn config set python /usr/bin/python3 - RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. diff --git a/packages/backend/package.json b/packages/backend/package.json index aed934a028..df06840aa0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.70-next.1", + "version": "0.2.71-next.0", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,39 +26,39 @@ "build-image": "docker build ../.. -f Dockerfile --tag example-backend" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/backend-tasks": "^0.3.0-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-app-backend": "^0.3.31-next.0", - "@backstage/plugin-auth-backend": "^0.13.0-next.1", - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/plugin-azure-devops-backend": "^0.3.10-next.0", - "@backstage/plugin-badges-backend": "^0.1.25-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", - "@backstage/plugin-code-coverage-backend": "^0.1.29-next.1", - "@backstage/plugin-graphql-backend": "^0.1.21-next.0", - "@backstage/plugin-jenkins-backend": "^0.1.20-next.1", - "@backstage/plugin-kubernetes-backend": "^0.4.14-next.0", - "@backstage/plugin-kafka-backend": "^0.2.24-next.0", - "@backstage/plugin-permission-backend": "^0.5.6-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-node": "^0.6.0-next.1", - "@backstage/plugin-proxy-backend": "^0.2.25-next.0", - "@backstage/plugin-rollbar-backend": "^0.1.28-next.0", - "@backstage/plugin-scaffolder-backend": "^1.1.0-next.1", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.6-next.1", - "@backstage/plugin-search-backend": "^0.5.0-next.1", - "@backstage/plugin-search-backend-node": "^0.5.3-next.0", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.1.3-next.0", - "@backstage/plugin-search-backend-module-pg": "^0.3.2-next.0", - "@backstage/plugin-techdocs-backend": "^1.0.1-next.1", - "@backstage/plugin-tech-insights-backend": "^0.3.0-next.1", - "@backstage/plugin-tech-insights-node": "^0.2.9-next.1", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.15-next.1", - "@backstage/plugin-todo-backend": "^0.1.28-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-app-backend": "^0.3.32-next.0", + "@backstage/plugin-auth-backend": "^0.13.1-next.0", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/plugin-azure-devops-backend": "^0.3.11-next.0", + "@backstage/plugin-badges-backend": "^0.1.26-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", + "@backstage/plugin-code-coverage-backend": "^0.1.30-next.0", + "@backstage/plugin-graphql-backend": "^0.1.22-next.0", + "@backstage/plugin-jenkins-backend": "^0.1.22-next.0", + "@backstage/plugin-kubernetes-backend": "^0.5.1-next.0", + "@backstage/plugin-kafka-backend": "^0.2.25-next.0", + "@backstage/plugin-permission-backend": "^0.5.7-next.0", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-node": "^0.6.1-next.0", + "@backstage/plugin-proxy-backend": "^0.2.26-next.0", + "@backstage/plugin-rollbar-backend": "^0.1.29-next.0", + "@backstage/plugin-scaffolder-backend": "^1.2.0-next.0", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.7-next.0", + "@backstage/plugin-search-backend": "^0.5.2-next.0", + "@backstage/plugin-search-backend-node": "^0.6.1-next.0", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.1.4-next.0", + "@backstage/plugin-search-backend-module-pg": "^0.3.3-next.0", + "@backstage/plugin-techdocs-backend": "^1.1.1-next.0", + "@backstage/plugin-tech-insights-backend": "^0.3.1-next.0", + "@backstage/plugin-tech-insights-node": "^0.2.10-next.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.16-next.0", + "@backstage/plugin-todo-backend": "^0.1.29-next.0", "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^18.5.3", "better-sqlite3": "^7.5.0", @@ -75,7 +75,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index da8eedcabc..8a60b4b4bd 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createRouter } from '@backstage/plugin-auth-backend'; +import { + createRouter, + providers, + defaultAuthProviderFactories, +} from '@backstage/plugin-auth-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -27,5 +31,64 @@ export default async function createPlugin( database: env.database, discovery: env.discovery, tokenManager: env.tokenManager, + providerFactories: { + ...defaultAuthProviderFactories, + + // NOTE: DO NOT add this many resolvers in your own instance! + // It is important that each real user always gets resolved to + // the same sign-in identity. The code below will not do that. + // It is here for demo purposes only. + github: providers.github.create({ + signIn: { + resolver: providers.github.resolvers.usernameMatchingUserEntityName(), + }, + }), + gitlab: providers.gitlab.create({ + signIn: { + async resolver({ result: { fullProfile } }, ctx) { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.id, + }, + }); + }, + }, + }), + microsoft: providers.microsoft.create({ + signIn: { + resolver: + providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + google: providers.google.create({ + signIn: { + resolver: + providers.google.resolvers.emailLocalPartMatchingUserEntityName(), + }, + }), + okta: providers.okta.create({ + signIn: { + resolver: + providers.okta.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + bitbucket: providers.bitbucket.create({ + signIn: { + resolver: + providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(), + }, + }), + onelogin: providers.onelogin.create({ + signIn: { + async resolver({ result: { fullProfile } }, ctx) { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.id, + }, + }); + }, + }, + }), + }, }); } diff --git a/packages/backend/src/plugins/techInsights.ts b/packages/backend/src/plugins/techInsights.ts index 94ce27656a..80ac559859 100644 --- a/packages/backend/src/plugins/techInsights.ts +++ b/packages/backend/src/plugins/techInsights.ts @@ -38,6 +38,7 @@ export default async function createPlugin( database: env.database, scheduler: env.scheduler, discovery: env.discovery, + tokenManager: env.tokenManager, factRetrievers: [ createFactRetrieverRegistration({ cadence: '1 1 1 * *', // Example cron, At 01:01 on day-of-month 1. diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 4830fd8fb0..968afe294b 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/catalog-client +## 1.0.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index b01635a0e9..a5af4e21c1 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "1.0.1-next.0", + "version": "1.0.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/errors": "^1.0.0", "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 89756babda..6dd90da2be 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/catalog-model +## 1.0.1 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- c12f8374d1: Added a connection between www-artist and artist-lookup components + +
+ Screenshots + + ![www-artist component](https://user-images.githubusercontent.com/74687/161088772-bc1df84e-28c1-4b08-b914-109cd6d04521.jpg) + + ![artist-lookup component](https://user-images.githubusercontent.com/74687/161088775-120c5610-f094-4cd1-bb2d-3ab9623a6c72.jpg) + +
+ ## 1.0.1-next.1 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 96ccca75cd..4154e1b8df 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "1.0.1-next.1", + "version": "1.0.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "@types/json-schema": "^7.0.5", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a6070daf8a..de67cbaf80 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/cli +## 0.17.1-next.0 + +### Patch Changes + +- 97cce67ac7: Add instructions to `create-github-app` command. +- 08e12a3a14: Add package global-agent to support behind a proxy for backstage-cli commands like versions:bump. +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 344ea56acc: Bump `commander` to version 9.1.0 +- 8ab2a8226b: Updated the `create-github-app` command to throw an error if the organization argument is a user or a non existing organization. +- Updated dependencies + - @backstage/config-loader@1.1.1-next.0 + +## 0.17.0 + +### Minor Changes + +- 1f7d4763ab: **BREAKING**: Bump the version range of `jest` from `^26.0.1` to `^27.5.1`. You can find the complete list of breaking changes [here](https://github.com/facebook/jest/releases/tag/v27.0.0). + + We strongly recommend to have completed the [package role migration](https://backstage.io/docs/tutorials/package-role-migration) before upgrading to this version, as the package roles are used to automatically determine the testing environment for each package. If you instead want to set an explicit test environment for each package, you can do so for example in the `"jest"` section in `package.json`. The default test environment for all packages is now `node`, which is also the new Jest default. + + Note that one of the breaking changes of Jest 27 is that the `jsdom` environment no longer includes `setImmediate` and `clearImmediate`, which means you might need to update some of your frontend packages. Another notable change is that `jest.useFakeTimers` now defaults to the `'modern'` implementation, which also mocks microtasks. + +### Patch Changes + +- e80ecad93c: Bump the `rushstack` api generator libraries to their latest versions +- c54ce828bd: build(deps): bump `eslint-plugin-jest` from 25.3.4 to 26.1.2 +- f151dfee5a: build(deps): bump `eslint-webpack-plugin` from 2.6.0 to 3.1.1 +- 7e7ba704be: build(deps): bump `@spotify/eslint-config-base` from 12.0.0 to 13.0.0 +- ecd72391fb: build(deps): bump `@spotify/eslint-config-typescript` +- 6a341b2d87: build(deps): bump `@spotify/eslint-config-react` from 12.0.0 to 13.0.0 +- 3c26b2edb5: build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0 +- ed3551b7be: Introduced a new experimental test configuration with a number of changes. It switches the coverage provider from `v8` to the default Babel provider, along with always enabling source maps in the Sucrase transform. It also adds a custom module loader that caches both file transforms and VM script objects across all projects in a test run, which provides a big performance boost when running tests from the project root, increasing speed and reducing memory usage. + + This new configuration is not enabled by default. It is enabled by setting the environment variable `BACKSTAGE_NEXT_TESTS` to a non-empty value. + +- 6ad0c45648: Added an experimental `package fix` command which applies automated fixes to the target package. The initial fix that is available is to add missing monorepo dependencies to the target package. +- 5b3079694e: Stop logging "Stopped watcher" when shutting down the development backend. +- f512554910: Updated the plugin template to install version 14 of `@testing-library/user-event`. + + To apply this change to your own project, update the `devDependencies` section in your `package.json` files: + + ```diff + "devDependencies": { + ... omitted dev dependencies ... + - "@testing-library/user-event": "^13.1.8", + + "@testing-library/user-event": "^14.0.0", + ... omitted dev dependencies ... + } + ``` + +- df7862cfa6: Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds. +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- 948a56f401: Added a new experimental `repo list-deprecations` command, which scans the entire project for usage of deprecated APIs. +- 4782f9e925: Fixed misleading log message during frontend plugin creation. +- 0383cd0228: The `versions:*` commands no longer warns about duplicate plugin libraries, such as `@backstage/plugin-catalog-common`. +- Updated dependencies + - @backstage/config-loader@1.1.0 + +## 0.17.0-next.3 + +### Patch Changes + +- e80ecad93c: Bump the `rushstack` api generator libraries to their latest versions +- 3c26b2edb5: build(deps): bump `npm-packlist` from 3.0.0 to 5.0.0 +- f512554910: Updated the plugin template to install version 14 of `@testing-library/user-event`. + + To apply this change to your own project, update the `devDependencies` section in your `package.json` files: + + ```diff + "devDependencies": { + ... omitted dev dependencies ... + - "@testing-library/user-event": "^13.1.8", + + "@testing-library/user-event": "^14.0.0", + ... omitted dev dependencies ... + } + ``` + +- df7862cfa6: Fixed a bug were the `react-hot-loader` transform was being applied to backend development builds. +- 230ad0826f: Bump to using `@types/node` v16 +- 0383cd0228: The `versions:*` commands no longer warns about duplicate plugin libraries, such as `@backstage/plugin-catalog-common`. +- Updated dependencies + - @backstage/config-loader@1.1.0-next.1 + ## 0.17.0-next.2 ### Patch Changes diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 6b9201559b..e5e08b4546 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -20,6 +20,11 @@ const crypto = require('crypto'); const glob = require('util').promisify(require('glob')); const { version } = require('../package.json'); +const envOptions = { + nextTests: Boolean(process.env.BACKSTAGE_NEXT_TESTS), + enableSourceMaps: Boolean(process.env.ENABLE_SOURCE_MAPS), +}; + const transformIgnorePattern = [ '@material-ui', '@rjsf', @@ -118,7 +123,7 @@ async function getProjectConfig(targetPath, displayName) { ...(displayName && { displayName }), rootDir: path.resolve(targetPath, 'src'), coverageDirectory: path.resolve(targetPath, 'coverage'), - coverageProvider: 'v8', + coverageProvider: envOptions.nextTests ? undefined : 'v8', collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], moduleNameMapper: { '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), @@ -127,7 +132,9 @@ async function getProjectConfig(targetPath, displayName) { transform: { '\\.(js|jsx|ts|tsx|mjs|cjs)$': [ require.resolve('./jestSucraseTransform.js'), - { enableSourceMaps: Boolean(process.env.ENABLE_SOURCE_MAPS) }, + { + enableSourceMaps: envOptions.enableSourceMaps || envOptions.nextTests, + }, ], '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': require.resolve('./jestFileTransform.js'), @@ -137,6 +144,10 @@ async function getProjectConfig(targetPath, displayName) { // A bit more opinionated testMatch: ['**/*.test.{js,jsx,ts,tsx,mjs,cjs}'], + moduleLoader: envOptions.nextTests + ? require.resolve('./jestCachingModuleLoader') + : undefined, + transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], ...getRoleConfig(closestPkgJson?.backstage?.role), diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js new file mode 100644 index 0000000000..9ef29d7485 --- /dev/null +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const { default: JestRuntime } = require('jest-runtime'); + +const fileTransformCache = new Map(); +const scriptTransformCache = new Map(); + +let runtimeGeneration = 0; + +module.exports = class CachingJestRuntime extends JestRuntime { + // Each Jest run creates a new runtime, including when rerunning tests in + // watch mode. This keeps track of whether we've switched runtime instance. + __runtimeGeneration = runtimeGeneration++; + + transformFile(filename, options) { + const entry = fileTransformCache.get(filename); + if (entry) { + // Only check modification time if it's from a different runtime generation + if (entry.generation === this.__runtimeGeneration) { + return entry.code; + } + + // Keep track of the modification time of files so that we can properly + // reprocess them in watch mode. + const { mtimeMs } = fs.statSync(filename); + if (mtimeMs > entry.mtimeMs) { + const code = super.transformFile(filename, options); + fileTransformCache.set(filename, { + code, + mtimeMs, + generation: this.__runtimeGeneration, + }); + return code; + } + + fileTransformCache.set(filename, { + ...entry, + generation: this.__runtimeGeneration, + }); + return entry.code; + } + + const code = super.transformFile(filename, options); + fileTransformCache.set(filename, { + code, + mtimeMs: fs.statSync(filename).mtimeMs, + generation: this.__runtimeGeneration, + }); + return code; + } + + // This may or may not be a good idea. Theoretically I don't know why this would impact + // test correctness and flakiness, but it seems like it may introduce flakiness and strange failures. + // It does seem to speed up test execution by a fair amount though. + createScriptFromCode(scriptSource, filename) { + let script = scriptTransformCache.get(scriptSource); + if (!script) { + script = super.createScriptFromCode(scriptSource, filename); + // Tried to store the script object in a WeakRef here. It starts out at + // about 90% hit rate, but eventually drops all the way to 20%, and overall + // it seemed to increase memory usage by 20% or so. + scriptTransformCache.set(scriptSource, script); + } + return script; + } +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index bd8964ef68..67af644dfa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.17.0-next.2", + "version": "0.17.1-next.0", "private": false, "publishConfig": { "access": "public" @@ -33,7 +33,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "@backstage/config": "^1.0.0", - "@backstage/config-loader": "^1.0.1-next.0", + "@backstage/config-loader": "^1.1.1-next.0", "@backstage/errors": "^1.0.0", "@backstage/release-manifests": "^0.0.2", "@backstage/types": "^1.0.0", @@ -62,7 +62,7 @@ "buffer": "^6.0.3", "chalk": "^4.0.0", "chokidar": "^3.3.1", - "commander": "^6.1.0", + "commander": "^9.1.0", "css-loader": "^6.5.1", "diff": "^5.0.0", "esbuild": "^0.14.10", @@ -80,18 +80,21 @@ "eslint-webpack-plugin": "^3.1.1", "express": "^4.17.1", "fork-ts-checker-webpack-plugin": "^7.0.0-alpha.8", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "glob": "^7.1.7", + "global-agent": "^3.0.0", "handlebars": "^4.7.3", "html-webpack-plugin": "^5.3.1", "inquirer": "^8.2.0", "jest": "^27.5.1", + "jest-runtime": "^27.5.1", "jest-css-modules": "^2.1.0", "jest-transform-yaml": "^1.0.0", "json-schema": "^0.4.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", "minimatch": "5.0.1", + "node-fetch": "^2.6.7", "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", @@ -122,13 +125,13 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@backstage/theme": "^0.2.15", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 67b71632c7..f90052bc83 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -15,20 +15,20 @@ */ import fs from 'fs-extra'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { buildBundle } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const { name } = await fs.readJson(paths.resolveTarget('package.json')); await buildBundle({ entry: 'src/index', parallelism: getEnvironmentParallelism(), - statsJsonEnabled: cmd.stats, + statsJsonEnabled: opts.stats, ...(await loadCliConfig({ - args: cmd.config, + args: opts.config, fromPackage: name, })), }); diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 716d47bd4c..e41c2367da 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -17,14 +17,14 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import uniq from 'lodash/uniq'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; import { Lockfile } from '../../lib/versioning'; import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const lockFilePath = paths.resolveTargetRoot('yarn.lock'); if (fs.existsSync(lockFilePath)) { try { @@ -80,9 +80,9 @@ export default async (cmd: Command) => { const { name } = await fs.readJson(paths.resolveTarget('package.json')); const waitForExit = await serveBundle({ entry: 'src/index', - checksEnabled: cmd.check, + checksEnabled: opts.check, ...(await loadCliConfig({ - args: cmd.config, + args: opts.config, fromPackage: name, withFilteredKeys: true, })), diff --git a/packages/cli/src/commands/backend/build.ts b/packages/cli/src/commands/backend/build.ts index 5d4e17e392..c85eecfed3 100644 --- a/packages/cli/src/commands/backend/build.ts +++ b/packages/cli/src/commands/backend/build.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { await buildPackage({ outputs: new Set([Output.cjs, Output.types]), - minify: cmd.minify, - useApiExtractor: cmd.experimentalTypeBuild, + minify: opts.minify, + useApiExtractor: opts.experimentalTypeBuild, }); }; diff --git a/packages/cli/src/commands/backend/bundle.ts b/packages/cli/src/commands/backend/bundle.ts index ec5fa0a131..78d4d72d92 100644 --- a/packages/cli/src/commands/backend/bundle.ts +++ b/packages/cli/src/commands/backend/bundle.ts @@ -18,7 +18,7 @@ import os from 'os'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import tar, { CreateOptions } from 'tar'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { createDistWorkspace } from '../../lib/packager'; import { paths } from '../../lib/paths'; import { getEnvironmentParallelism } from '../../lib/parallel'; @@ -27,7 +27,7 @@ import { buildPackage, Output } from '../../lib/builder'; const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const targetDir = paths.resolveTarget('dist'); const pkg = await fs.readJson(paths.resolveTarget('package.json')); @@ -38,7 +38,7 @@ export default async (cmd: Command) => { try { await createDistWorkspace([pkg.name], { targetDir: tmpDir, - buildDependencies: Boolean(cmd.buildDependencies), + buildDependencies: Boolean(opts.buildDependencies), buildExcludes: [pkg.name], parallelism: getEnvironmentParallelism(), skeleton: SKELETON_FILE, diff --git a/packages/cli/src/commands/backend/dev.ts b/packages/cli/src/commands/backend/dev.ts index b14a1f3e20..2ee43958a2 100644 --- a/packages/cli/src/commands/backend/dev.ts +++ b/packages/cli/src/commands/backend/dev.ts @@ -15,11 +15,11 @@ */ import fs from 'fs-extra'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { paths } from '../../lib/paths'; import { serveBackend } from '../../lib/bundler/backend'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { // Cleaning dist/ before we start the dev process helps work around an issue // where we end up with the entrypoint executing multiple times, causing // a port bind conflict among other things. @@ -27,9 +27,9 @@ export default async (cmd: Command) => { const waitForExit = await serveBackend({ entry: 'src/index', - checksEnabled: cmd.check, - inspectEnabled: cmd.inspect, - inspectBrkEnabled: cmd.inspectBrk, + checksEnabled: opts.check, + inspectEnabled: opts.inspect, + inspectBrkEnabled: opts.inspectBrk, }); await waitForExit(); diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 5c8065dbb4..fcaf58e1ee 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; import { paths } from '../../lib/paths'; import { buildFrontend } from './buildFrontend'; import { buildBackend } from './buildBackend'; -export async function command(cmd: Command): Promise { - const role = await findRoleFromCommand(cmd); +export async function command(opts: OptionValues): Promise { + const role = await findRoleFromCommand(opts); if (role === 'frontend') { return buildFrontend({ targetDir: paths.targetDir, - configPaths: cmd.config as string[], - writeStats: Boolean(cmd.stats), + configPaths: opts.config as string[], + writeStats: Boolean(opts.stats), }); } if (role === 'backend') { return buildBackend({ targetDir: paths.targetDir, - skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + skipBuildDependencies: Boolean(opts.skipBuildDependencies), }); } @@ -54,7 +54,7 @@ export async function command(cmd: Command): Promise { return buildPackage({ outputs, - minify: Boolean(cmd.minify), - useApiExtractor: Boolean(cmd.experimentalTypeBuild), + minify: Boolean(opts.minify), + useApiExtractor: Boolean(opts.experimentalTypeBuild), }); } diff --git a/packages/cli/src/commands/buildWorkspace.ts b/packages/cli/src/commands/buildWorkspace.ts index 7a7aa28ad6..97bf2f1239 100644 --- a/packages/cli/src/commands/buildWorkspace.ts +++ b/packages/cli/src/commands/buildWorkspace.ts @@ -15,10 +15,9 @@ */ import fs from 'fs-extra'; -import { Command } from 'commander'; import { createDistWorkspace } from '../lib/packager'; -export default async (dir: string, _cmd: Command, packages: string[]) => { +export default async (dir: string, packages: string[]) => { if (!(await fs.pathExists(dir))) { throw new Error(`Target workspace directory doesn't exist, '${dir}'`); } diff --git a/packages/cli/src/commands/config/docs.ts b/packages/cli/src/commands/config/docs.ts index 7a807f7c78..98e683e87c 100644 --- a/packages/cli/src/commands/config/docs.ts +++ b/packages/cli/src/commands/config/docs.ts @@ -16,17 +16,17 @@ import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; import { loadCliConfig } from '../../lib/config'; const DOCS_URL = 'https://config.backstage.io'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const { schema: appSchemas } = await loadCliConfig({ args: [], - fromPackage: cmd.package, + fromPackage: opts.package, mockEnv: true, }); diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/commands/config/print.ts index b640ee4f37..9ef725ebf8 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/commands/config/print.ts @@ -14,36 +14,36 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; import { loadCliConfig } from '../../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const { schema, appConfigs } = await loadCliConfig({ - args: cmd.config, - fromPackage: cmd.package, - mockEnv: cmd.lax, - fullVisibility: !cmd.frontend, + args: opts.config, + fromPackage: opts.package, + mockEnv: opts.lax, + fullVisibility: !opts.frontend, }); - const visibility = getVisibilityOption(cmd); + const visibility = getVisibilityOption(opts); const data = serializeConfigData(appConfigs, schema, visibility); - if (cmd.format === 'json') { + if (opts.format === 'json') { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(data)}\n`); } }; -function getVisibilityOption(cmd: Command): ConfigVisibility { - if (cmd.frontend && cmd.withSecrets) { +function getVisibilityOption(opts: OptionValues): ConfigVisibility { + if (opts.frontend && opts.withSecrets) { throw new Error('Not allowed to combine frontend and secret config'); } - if (cmd.frontend) { + if (opts.frontend) { return 'frontend'; - } else if (cmd.withSecrets) { + } else if (opts.withSecrets) { return 'secret'; } return 'backend'; diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/commands/config/schema.ts index be228e1e7c..f9e6573be6 100644 --- a/packages/cli/src/commands/config/schema.ts +++ b/packages/cli/src/commands/config/schema.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const { schema } = await loadCliConfig({ args: [], - fromPackage: cmd.package, + fromPackage: opts.package, mockEnv: true, }); @@ -38,7 +38,7 @@ export default async (cmd: Command) => { merged.description = 'This is the schema describing the structure of the app-config.yaml configuration file.'; - if (cmd.format === 'json') { + if (opts.format === 'json') { process.stdout.write(`${JSON.stringify(merged, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(merged)}\n`); diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts index 8d528b8300..c3d05291e0 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/commands/config/validate.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { loadCliConfig } from '../../lib/config'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { await loadCliConfig({ - args: cmd.config, - fromPackage: cmd.package, - mockEnv: cmd.lax, - fullVisibility: !cmd.frontend, - withDeprecatedKeys: cmd.deprecated, + args: opts.config, + fromPackage: opts.package, + mockEnv: opts.lax, + fullVisibility: !opts.frontend, + withDeprecatedKeys: opts.deprecated, }); }; diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index c62234c636..32fa1e9a6d 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -19,11 +19,13 @@ import chalk from 'chalk'; import { stringify as stringifyYaml } from 'yaml'; import { paths } from '../../lib/paths'; import { GithubCreateAppServer } from './GithubCreateAppServer'; +import fetch from 'node-fetch'; // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { + await verifyGithubOrg(org); const { slug, name, ...config } = await GithubCreateAppServer.run({ org }); const fileName = `github-app-${slug}-credentials.yaml`; @@ -35,5 +37,38 @@ export default async (org: string) => { 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', ), ); - // TODO: log instructions on how to use the newly created app configuration. + console.log( + "Here's an example on how to update the integrations section in app-config.yaml", + ); + console.log( + chalk.green(` +integrations: + github: + - host: github.com + apps: + - $include: ${fileName}`), + ); }; + +async function verifyGithubOrg(org: string): Promise { + let response; + + try { + response = await fetch( + `https://api.github.com/orgs/${encodeURIComponent(org)}`, + ); + } catch (e) { + console.log( + chalk.yellow( + 'Warning: Unable to verify existence of GitHub organization. ', + e, + ), + ); + } + + if (response?.status === 404) { + throw new Error( + `GitHub organization '${org}' does not exist. Please verify the name and try again.`, + ); + } +} diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index c80129662c..a12060bbd2 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -23,7 +23,7 @@ import { resolve as resolvePath, join as joinPath } from 'path'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import os from 'os'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { assertError } from '@backstage/errors'; import { parseOwnerIds, @@ -195,7 +195,7 @@ export async function movePlugin( }); } -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ @@ -242,21 +242,21 @@ export default async (cmd: Command) => { const answers: Answers = await inquirer.prompt(questions); const pluginId = - cmd.backend && !answers.id.endsWith('-backend') + opts.backend && !answers.id.endsWith('-backend') ? `${answers.id}-backend` : answers.id; - const name = cmd.scope - ? `@${cmd.scope.replace(/^@/, '')}/plugin-${pluginId}` + const name = opts.scope + ? `@${opts.scope.replace(/^@/, '')}/plugin-${pluginId}` : `plugin-${pluginId}`; const pluginVar = `${camelCase(answers.id)}Plugin`; const extensionName = `${upperFirst(camelCase(answers.id))}Page`; - const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : ''; - const privatePackage = cmd.private === false ? false : true; + const npmRegistry = opts.npmRegistry && opts.scope ? opts.npmRegistry : ''; + const privatePackage = opts.private === false ? false : true; const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); const appPackage = paths.resolveTargetRoot('packages/app'); const templateDir = paths.resolveOwn( - cmd.backend + opts.backend ? 'templates/default-backend-plugin' : 'templates/default-plugin', ); @@ -309,7 +309,7 @@ export default async (cmd: Command) => { Task.section('Building the plugin'); await buildPlugin(pluginDir); - if ((await fs.pathExists(appPackage)) && !cmd.backend) { + if ((await fs.pathExists(appPackage)) && !opts.backend) { Task.section('Adding plugin as dependency in app'); await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion); diff --git a/packages/cli/src/commands/create/create.ts b/packages/cli/src/commands/create/create.ts index 64067e1b7e..c784307738 100644 --- a/packages/cli/src/commands/create/create.ts +++ b/packages/cli/src/commands/create/create.ts @@ -17,7 +17,7 @@ import os from 'os'; import fs from 'fs-extra'; import { join as joinPath } from 'path'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { FactoryRegistry } from '../../lib/create/FactoryRegistry'; import { paths } from '../../lib/paths'; import { assertError } from '@backstage/errors'; @@ -40,12 +40,10 @@ function parseOptions(optionStrings: string[]): Record { return options; } -export default async (cmd: Command) => { - const cmdOpts = cmd.opts(); +export default async (opts: OptionValues) => { + const factory = await FactoryRegistry.interactiveSelect(opts.select); - const factory = await FactoryRegistry.interactiveSelect(cmdOpts.select); - - const providedOptions = parseOptions(cmdOpts.option); + const providedOptions = parseOptions(opts.option); const options = await FactoryRegistry.populateOptions( factory, providedOptions, @@ -93,9 +91,9 @@ export default async (cmd: Command) => { await factory.create(options, { isMonoRepo, defaultVersion, - scope: cmdOpts.scope?.replace(/^@/, ''), - npmRegistry: cmdOpts.npmRegistry, - private: Boolean(cmdOpts.private), + scope: opts.scope?.replace(/^@/, ''), + npmRegistry: opts.npmRegistry, + private: Boolean(opts.private), createTemporaryDirectory, markAsModified() { modified = true; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 2f345e8663..1a7b2c4f08 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -15,17 +15,17 @@ */ import { assertError } from '@backstage/errors'; -import { CommanderStatic } from 'commander'; +import { Command } from 'commander'; import { exitWithError } from '../lib/errors'; const configOption = [ '--config ', 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => [...opts, opt], + (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), Array(), ] as const; -export function registerRepoCommand(program: CommanderStatic) { +export function registerRepoCommand(program: Command) { const command = program .command('repo [command]') .description('Command that run across an entire Backstage project'); @@ -69,7 +69,7 @@ export function registerRepoCommand(program: CommanderStatic) { ); } -export function registerScriptCommand(program: CommanderStatic) { +export function registerScriptCommand(program: Command) { const command = program .command('package [command]') .description('Lifecycle scripts for individual packages'); @@ -110,13 +110,13 @@ export function registerScriptCommand(program: CommanderStatic) { .option( '--config ', 'Config files to load instead of app-config.yaml. Applies to app packages only.', - (opt: string, opts: string[]) => [...opts, opt], + (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), Array(), ) .action(lazy(() => import('./build').then(m => m.command))); command - .command('lint') + .command('lint [directories...]') .option( '--format ', 'Lint report output format', @@ -131,7 +131,7 @@ export function registerScriptCommand(program: CommanderStatic) { .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args .helpOption(', --backstage-cli-help') // Let Jest handle help .description('Run tests, forwarding args to Jest, defaulting to watch mode') - .action(lazy(() => import('./testCommand').then(m => m.default))); + .action(lazy(() => import('./test').then(m => m.default))); command .command('fix', { hidden: true }) @@ -155,7 +155,7 @@ export function registerScriptCommand(program: CommanderStatic) { .action(lazy(() => import('./pack').then(m => m.post))); } -export function registerMigrateCommand(program: CommanderStatic) { +export function registerMigrateCommand(program: Command) { const command = program .command('migrate [command]') .description('Migration utilities'); @@ -182,7 +182,7 @@ export function registerMigrateCommand(program: CommanderStatic) { ); } -export function registerCommands(program: CommanderStatic) { +export function registerCommands(program: Command) { // TODO(Rugvip): Deprecate in favor of package variant program .command('app:build') @@ -302,7 +302,7 @@ export function registerCommands(program: CommanderStatic) { // TODO(Rugvip): Deprecate in favor of package variant program - .command('lint') + .command('lint [directories...]') .option( '--format ', 'Lint report output format', @@ -320,7 +320,7 @@ export function registerCommands(program: CommanderStatic) { .description( 'Run tests, forwarding args to Jest, defaulting to watch mode [DEPRECATED]', ) - .action(lazy(() => import('./testCommand').then(m => m.default))); + .action(lazy(() => import('./test').then(m => m.default))); program .command('config:docs') @@ -423,7 +423,7 @@ export function registerCommands(program: CommanderStatic) { .action(lazy(() => import('./clean/clean').then(m => m.default))); program - .command('build-workspace ...') + .command('build-workspace [packages...]') .description('Builds a temporary dist workspace from the provided packages') .action(lazy(() => import('./buildWorkspace').then(m => m.default))); diff --git a/packages/cli/src/commands/install/install.ts b/packages/cli/src/commands/install/install.ts index bd3a05ebc1..e395088b3e 100644 --- a/packages/cli/src/commands/install/install.ts +++ b/packages/cli/src/commands/install/install.ts @@ -22,7 +22,7 @@ import { import { fetchPackageInfo } from '../../lib/versioning'; import { NotFoundError } from '../../lib/errors'; import * as stepDefinitionMap from './steps'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import fs from 'fs-extra'; const stepDefinitions = Object.values(stepDefinitionMap); @@ -154,7 +154,7 @@ async function loadPeerPluginDeps( } } -export default async (pluginId?: string, cmd?: Command) => { +export default async (pluginId?: string, cmd?: OptionValues) => { const from = pluginId || cmd?.from; // TODO(himanshu): If no plugin id is provided, it should list all plugins available. Maybe in some other command? if (!from) { diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 823d24fbe8..87c353f56f 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -14,27 +14,29 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { paths } from '../lib/paths'; import { ESLint } from 'eslint'; -export default async (cmd: Command, cmdArgs: string[]) => { +export default async (directories: string[], opts: OptionValues) => { const eslint = new ESLint({ cwd: paths.targetDir, - fix: cmd.fix, + fix: opts.fix, extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], }); - const results = await eslint.lintFiles(cmdArgs ?? ['.']); + const results = await eslint.lintFiles( + directories.length ? directories : ['.'], + ); - if (cmd.fix) { + if (opts.fix) { await ESLint.outputFixes(results); } - const formatter = await eslint.loadFormatter(cmd.format); + const formatter = await eslint.loadFormatter(opts.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') { + if (opts.format === 'eslint-formatter-friendly') { process.chdir(paths.targetRoot); } const resultText = formatter.format(results); diff --git a/packages/cli/src/commands/oldBuild.ts b/packages/cli/src/commands/oldBuild.ts index 86ab5c4300..c0899d4d02 100644 --- a/packages/cli/src/commands/oldBuild.ts +++ b/packages/cli/src/commands/oldBuild.ts @@ -15,12 +15,12 @@ */ import { buildPackage, Output } from '../lib/builder'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { let outputs = new Set(); - const { outputs: outputsStr } = cmd as { outputs?: string }; + const { outputs: outputsStr } = opts as { outputs?: string }; if (outputsStr) { for (const output of outputsStr.split(',') as (keyof typeof Output)[]) { if (output in Output) { @@ -35,7 +35,7 @@ export default async (cmd: Command) => { await buildPackage({ outputs, - minify: cmd.minify, - useApiExtractor: cmd.experimentalTypeBuild, + minify: opts.minify, + useApiExtractor: opts.experimentalTypeBuild, }); }; diff --git a/packages/cli/src/commands/plugin/build.ts b/packages/cli/src/commands/plugin/build.ts index 56f99f59b1..671aece818 100644 --- a/packages/cli/src/commands/plugin/build.ts +++ b/packages/cli/src/commands/plugin/build.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { await buildPackage({ outputs: new Set([Output.esm, Output.types]), - minify: cmd.minify, - useApiExtractor: cmd.experimentalTypeBuild, + minify: opts.minify, + useApiExtractor: opts.experimentalTypeBuild, }); }; diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 9a2a3b2e3e..07f2a94ea8 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { diffTemplateFiles, handlers, @@ -50,13 +50,13 @@ const fileHandlers = [ }, ]; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { let promptFunc = inquirerPromptFunc; let finalize = () => {}; - if (cmd.check) { + if (opts.check) { [promptFunc, finalize] = makeCheckPromptFunc(); - } else if (cmd.yes) { + } else if (opts.yes) { promptFunc = yesPromptFunc; } diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index a0200b7ce8..891db190e0 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -15,18 +15,18 @@ */ import fs from 'fs-extra'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { serveBundle } from '../../lib/bundler'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const { name } = await fs.readJson(paths.resolveTarget('package.json')); const waitForExit = await serveBundle({ entry: 'dev/index', - checksEnabled: cmd.check, + checksEnabled: opts.check, ...(await loadCliConfig({ - args: cmd.config, + args: opts.config, fromPackage: name, withFilteredKeys: true, })), diff --git a/packages/cli/src/commands/plugin/testCommand.ts b/packages/cli/src/commands/plugin/test.ts similarity index 85% rename from packages/cli/src/commands/plugin/testCommand.ts rename to packages/cli/src/commands/plugin/test.ts index e1ff0f36f9..ca076ae6fc 100644 --- a/packages/cli/src/commands/plugin/testCommand.ts +++ b/packages/cli/src/commands/plugin/test.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { run } from '../../lib/run'; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const args = ['test']; - if (cmd.watch) { + if (opts.watch) { args.push('--watch'); } - if (cmd.coverage) { + if (opts.coverage) { args.push('--coverage'); } diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index fe50bee84f..21b1580cb9 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -15,7 +15,7 @@ */ import chalk from 'chalk'; -import { Command } from 'commander'; +import { Command, OptionValues } from 'commander'; import { relative as relativePath } from 'path'; import { buildPackages, getOutputsForRole } from '../../lib/builder'; import { PackageGraph } from '../../lib/monorepo'; @@ -60,29 +60,31 @@ function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { // 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 currentOpts = (cmd as any)._optionValues; + const currentStore = (cmd as any)._storeOptionsAsProperties; const result: Record = {}; - cmd._storeOptionsAsProperties = false; - cmd._optionValues = result; + (cmd as any)._storeOptionsAsProperties = false; + (cmd as any)._optionValues = result; // Triggers the writing of options to the result object cmd.parseOptions(argsStr.split(' ')); - cmd._storeOptionsAsProperties = currentOpts; - cmd._optionValues = currentStore; + (cmd as any)._storeOptionsAsProperties = currentOpts; + (cmd as any)._optionValues = currentStore; return result; }; } -export async function command(cmd: Command): Promise { +export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); - if (cmd.since) { + if (opts.since) { const graph = PackageGraph.fromPackages(packages); - const changedPackages = await graph.listChangedPackages({ ref: cmd.since }); + const changedPackages = await graph.listChangedPackages({ + ref: opts.since, + }); const withDevDependents = graph.collectPackageNames( changedPackages.map(pkg => pkg.name), pkg => pkg.localDevDependents.keys(), @@ -137,7 +139,7 @@ export async function command(cmd: Command): Promise { console.log('Building packages'); await buildPackages(options); - if (cmd.all) { + if (opts.all) { console.log('Building apps'); await runParallelWorkers({ items: apps, diff --git a/packages/cli/src/commands/repo/lint.ts b/packages/cli/src/commands/repo/lint.ts index b054dabe34..19e034aa7d 100644 --- a/packages/cli/src/commands/repo/lint.ts +++ b/packages/cli/src/commands/repo/lint.ts @@ -15,7 +15,7 @@ */ import chalk from 'chalk'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { relative as relativePath } from 'path'; import { PackageGraph, ExtendedPackageJSON } from '../../lib/monorepo'; import { runWorkerQueueThreads } from '../../lib/parallel'; @@ -29,12 +29,12 @@ function depCount(pkg: ExtendedPackageJSON) { return deps + devDeps; } -export async function command(cmd: Command): Promise { +export async function command(opts: OptionValues): Promise { let packages = await PackageGraph.listTargetPackages(); - if (cmd.since) { + if (opts.since) { const graph = PackageGraph.fromPackages(packages); - packages = await graph.listChangedPackages({ ref: cmd.since }); + packages = await graph.listChangedPackages({ ref: opts.since }); } // Packages are ordered from most to least number of dependencies, as a @@ -42,7 +42,7 @@ export async function command(cmd: Command): Promise { packages.sort((a, b) => depCount(b.packageJson) - depCount(a.packageJson)); // 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') { + if (opts.format === 'eslint-formatter-friendly') { process.chdir(paths.targetRoot); } @@ -57,8 +57,8 @@ export async function command(cmd: Command): Promise { relativeDir: relativePath(paths.targetRoot, pkg.dir), })), workerData: { - fix: Boolean(cmd.fix), - format: cmd.format as string | undefined, + fix: Boolean(opts.fix), + format: opts.format as string | undefined, }, workerFactory: async ({ fix, format }) => { const { ESLint } = require('eslint'); diff --git a/packages/cli/src/commands/repo/list-deprecations.ts b/packages/cli/src/commands/repo/list-deprecations.ts index b53118cc40..97fa384561 100644 --- a/packages/cli/src/commands/repo/list-deprecations.ts +++ b/packages/cli/src/commands/repo/list-deprecations.ts @@ -16,12 +16,12 @@ import chalk from 'chalk'; import { ESLint } from 'eslint'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { join as joinPath, relative as relativePath } from 'path'; import { paths } from '../../lib/paths'; import { PackageGraph } from '../../lib/monorepo'; -export async function command(cmd: Command) { +export async function command(opts: OptionValues) { const packages = await PackageGraph.listTargetPackages(); const eslint = new ESLint({ @@ -74,7 +74,7 @@ export async function command(cmd: Command) { stderr.cursorTo(0); } - if (cmd.json) { + if (opts.json) { console.log(JSON.stringify(deprecations, null, 2)); } else { for (const d of deprecations) { diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 7a11028800..9306ab247c 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { startBackend } from './startBackend'; import { startFrontend } from './startFrontend'; import { findRoleFromCommand } from '../../lib/role'; -export async function command(cmd: Command): Promise { - const role = await findRoleFromCommand(cmd); +export async function command(opts: OptionValues): Promise { + const role = await findRoleFromCommand(opts); const options = { - configPaths: cmd.config as string[], - checksEnabled: Boolean(cmd.check), - inspectEnabled: Boolean(cmd.inspect), - inspectBrkEnabled: Boolean(cmd.inspectBrk), + configPaths: opts.config as string[], + checksEnabled: Boolean(opts.check), + inspectEnabled: Boolean(opts.inspect), + inspectBrkEnabled: Boolean(opts.inspectBrk), }; switch (role) { diff --git a/packages/cli/src/commands/testCommand.ts b/packages/cli/src/commands/test.ts similarity index 92% rename from packages/cli/src/commands/testCommand.ts rename to packages/cli/src/commands/test.ts index 7165b20925..f1bfcc356b 100644 --- a/packages/cli/src/commands/testCommand.ts +++ b/packages/cli/src/commands/test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { Command, OptionValues } from 'commander'; import { paths } from '../lib/paths'; import { runCheck } from '../lib/run'; @@ -27,14 +27,14 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { return false; } -export default async (cmd: Command) => { +export default async (_opts: OptionValues, cmd: Command) => { // all args are forwarded to jest let parent = cmd; while (parent.parent) { parent = parent.parent; } - const rawArgs = parent.rawArgs as string[]; - const args = rawArgs.slice(rawArgs.indexOf('test') + 1); + const allArgs = parent.args as string[]; + const args = allArgs.slice(allArgs.indexOf('test') + 1); // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index b9d318bccc..0e042d1cba 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -19,7 +19,7 @@ import chalk from 'chalk'; import ora from 'ora'; import semver from 'semver'; import minimatch from 'minimatch'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { run } from '../../lib/run'; @@ -38,6 +38,7 @@ import { getManifestByVersion, ReleaseManifest, } from '@backstage/release-manifests'; +import 'global-agent/bootstrap'; const DEP_TYPES = [ 'dependencies', @@ -55,10 +56,10 @@ type PkgVersionInfo = { location: string; }; -export default async (cmd: Command) => { +export default async (opts: OptionValues) => { const lockfilePath = paths.resolveTargetRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); - let pattern = cmd.pattern; + let pattern = opts.pattern; if (!pattern) { console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`); @@ -70,14 +71,14 @@ export default async (cmd: Command) => { let findTargetVersion: (name: string) => Promise; let releaseManifest: ReleaseManifest; // Specific release specified. Be strict when resolving versions - if (semver.valid(cmd.release)) { - releaseManifest = await getManifestByVersion({ version: cmd.release }); + if (semver.valid(opts.release)) { + releaseManifest = await getManifestByVersion({ version: opts.release }); findTargetVersion = createStrictVersionFinder({ releaseManifest, }); } else { // Release line specified. Be lenient when resolving versions. - if (cmd.release === 'next') { + if (opts.release === 'next') { const next = await getManifestByReleaseLine({ releaseLine: 'next', }); @@ -90,11 +91,11 @@ export default async (cmd: Command) => { : main; } else { releaseManifest = await getManifestByReleaseLine({ - releaseLine: cmd.release, + releaseLine: opts.release, }); } findTargetVersion = createVersionFinder({ - releaseLine: cmd.releaseLine, + releaseLine: opts.releaseLine, releaseManifest, }); } diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index 22f7961db6..9939b2ba51 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { Lockfile } from '../../lib/versioning'; import { paths } from '../../lib/paths'; import partition from 'lodash/partition'; @@ -47,7 +47,7 @@ export const forbiddenDuplicatesFilter = (name: string) => FORBID_DUPLICATES.some(pattern => pattern.test(name)) && !ALLOW_DUPLICATES.some(pattern => pattern.test(name)); -export default async (cmd: Command) => { +export default async (cmd: OptionValues) => { const fix = Boolean(cmd.fix); let success = true; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 286ce6f4d6..d1ca1bcd04 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -20,7 +20,7 @@ * @packageDocumentation */ -import program from 'commander'; +import { program } from 'commander'; import chalk from 'chalk'; import { exitWithError } from './lib/errors'; import { version } from './lib/version'; diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index fe91cedbb0..1cfaf15bfc 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -154,6 +154,16 @@ export async function makeRollupConfigs( file: resolvePath(distDir, 'index.d.ts'), format: 'es', }, + external: [ + /\.css$/, + /\.scss$/, + /\.sass$/, + /\.svg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], onwarn, plugins: [dts()], }); diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli/src/lib/parallel.ts index 7b4ae4609e..ab32de992c 100644 --- a/packages/cli/src/lib/parallel.ts +++ b/packages/cli/src/lib/parallel.ts @@ -213,6 +213,7 @@ export async function runWorkerQueueThreads( return results; } +/* istanbul ignore next */ function workerQueueThread( workerFuncFactory: ( data: unknown, @@ -313,6 +314,7 @@ export async function runWorkerThreads( ); } +/* istanbul ignore next */ function workerThread( workerFunc: ( data: unknown, diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index 2772d263f1..2cd1e29368 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -79,9 +79,10 @@ describe('getRoleFromPackage', () => { describe('findRoleFromCommand', () => { function mkCommand(args: string) { - return new Command() + const parsed = new Command() .option('--role ', 'test role') .parse(['node', 'entry.js', ...args.split(' ')]) as Command; + return parsed.opts(); } beforeEach(() => { diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index a33a7d715a..685848fd26 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -16,7 +16,7 @@ import { z } from 'zod'; import fs from 'fs-extra'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { paths } from '../paths'; import { PackageRole, PackageRoleInfo } from './types'; @@ -108,9 +108,11 @@ export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { return undefined; } -export async function findRoleFromCommand(cmd: Command): Promise { - if (cmd.role) { - return getRoleInfo(cmd.role)?.role; +export async function findRoleFromCommand( + opts: OptionValues, +): Promise { + if (opts.role) { + return getRoleInfo(opts.role)?.role; } const pkg = await fs.readJson(paths.resolveTarget('package.json')); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 4462da88e9..9b3aec5d0e 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/codemods +## 0.1.38-next.0 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 + +## 0.1.37 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + +## 0.1.37-next.0 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + ## 0.1.36 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index bee26c5f53..f1700564eb 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.36", + "version": "0.1.38-next.0", "private": false, "publishConfig": { "access": "public", @@ -42,7 +42,7 @@ "devDependencies": { "@types/jscodeshift": "^0.11.0", "@types/node": "^16.11.26", - "commander": "^6.1.0", + "commander": "^9.1.0", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/codemods/src/action.ts b/packages/codemods/src/action.ts index bc9104425e..b955ab17ea 100644 --- a/packages/codemods/src/action.ts +++ b/packages/codemods/src/action.ts @@ -16,7 +16,7 @@ import { relative as relativePath } from 'path'; import { spawn } from 'child_process'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { findPaths } from '@backstage/cli-common'; import { platform } from 'os'; import { ExitCodeError } from './errors'; @@ -25,7 +25,7 @@ import { ExitCodeError } from './errors'; const paths = findPaths(__dirname); export function createCodemodAction(name: string) { - return async (_: unknown, cmd: Command) => { + return async (dirs: string[], opts: OptionValues) => { const transformPath = relativePath( process.cwd(), paths.resolveOwn('transforms', `${name}.js`), @@ -39,12 +39,12 @@ export function createCodemodAction(name: string) { '--ignore-pattern=**/node_modules/**', ]; - if (cmd.dry) { + if (opts.dry) { args.push('--dry'); } - if (cmd.args.length) { - args.push(...cmd.args); + if (dirs.length) { + args.push(...dirs); } else { args.push('.'); } diff --git a/packages/codemods/src/index.ts b/packages/codemods/src/index.ts index 8eb8840cf0..ef408e5053 100644 --- a/packages/codemods/src/index.ts +++ b/packages/codemods/src/index.ts @@ -20,7 +20,7 @@ * @packageDocumentation */ -import program from 'commander'; +import { program } from 'commander'; import chalk from 'chalk'; import { codemods } from './codemods'; import { exitWithError } from './errors'; @@ -31,14 +31,14 @@ async function main(argv: string[]) { program.name('backstage-codemods').version(version); const applyCommand = program - .command('apply []') + .command('apply [target-dirs...]') .description( 'Apply a codemod to target directories, defaulting to the current directory', ); for (const codemod of codemods) { applyCommand - .command(`${codemod.name} []`) + .command(`${codemod.name} [target-dirs...]`) .description(codemod.description) .option('-d, --dry', 'Dry run, no changes written to files') .action(createCodemodAction(codemod.name)); diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 3340633857..894be08dc0 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/config-loader +## 1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully + +## 1.1.0 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## 1.1.0-next.1 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 56c3eefee0..aa40320ac2 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "1.0.1-next.0", + "version": "1.1.1-next.0", "private": false, "publishConfig": { "access": "public", @@ -41,7 +41,7 @@ "@types/json-schema": "^7.0.6", "ajv": "^8.10.0", "chokidar": "^3.5.2", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "json-schema": "^0.4.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 058f59aef1..8c4d0b48a4 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -104,6 +104,7 @@ describe('loadConfig', () => { `, '/root/secrets/substituted.txt': '123abc', '/root/${ESCAPE_ME}.txt': 'notSubstituted', + '/root/empty.yaml': '# just a comment', }); }); @@ -469,6 +470,17 @@ describe('loadConfig', () => { await new Promise(resolve => setTimeout(resolve, 1000)); }); + it('handles empty files gracefully', async () => { + await expect( + loadConfig({ + configRoot: '/root', + configTargets: [{ path: '/root/empty.yaml' }], + }), + ).resolves.toEqual({ + appConfigs: [], + }); + }); + function defer() { let resolve: (value: T) => void; const promise = new Promise(_resolve => { diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 7ceb2e8548..9537d2d945 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -160,13 +160,17 @@ export async function loadConfig( }; const input = yaml.parse(await readFile(configPath)); - const substitutionTransform = createSubstitutionTransform(env); - const data = await applyConfigTransforms(dir, input, [ - createIncludeTransform(env, readFile, substitutionTransform), - substitutionTransform, - ]); - fileConfigs.push({ data, context: basename(configPath) }); + // A completely empty file ends up as a null return value + if (input !== null) { + const substitutionTransform = createSubstitutionTransform(env); + const data = await applyConfigTransforms(dir, input, [ + createIncludeTransform(env, readFile, substitutionTransform), + substitutionTransform, + ]); + + fileConfigs.push({ data, context: basename(configPath) }); + } } return { fileConfigs, loadedPaths }; diff --git a/packages/config/package.json b/packages/config/package.json index 4f711b76e7..3fda38f6a4 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -37,7 +37,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^1.0.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 3d3e851c80..675365ed27 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/core-app-api +## 1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. +- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + +## 1.0.1-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 68763cd306..e7ba7fcf47 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.0.1-next.0", + "version": "1.0.1", "private": false, "publishConfig": { "access": "public", @@ -34,9 +34,9 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/types": "^1.0.0", - "@backstage/version-bridge": "^1.0.0", + "@backstage/version-bridge": "^1.0.1", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -49,11 +49,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 0262ece8f4..75bab5c2df 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -17,8 +17,10 @@ import { Config } from '@backstage/config'; import React, { ComponentType, + createContext, PropsWithChildren, ReactElement, + useContext, useEffect, useMemo, useState, @@ -55,9 +57,7 @@ import { import { pluginCollector } from '../plugins/collectors'; import { featureFlagCollector, - routeObjectCollector, - routeParentCollector, - routePathCollector, + routingV1Collector, } from '../routing/collectors'; import { RoutingProvider } from '../routing/RoutingProvider'; import { RouteTracker } from '../routing/RouteTracker'; @@ -79,6 +79,7 @@ import { AppThemeProvider } from './AppThemeProvider'; import { defaultConfigLoader } from './defaultConfigLoader'; import { ApiRegistry } from '../apis/system/ApiRegistry'; import { resolveRouteBindings } from './resolveRouteBindings'; +import { BackstageRouteObject } from '../routing/types'; type CompatiblePlugin = | BackstagePlugin @@ -86,6 +87,10 @@ type CompatiblePlugin = output(): Array<{ type: 'feature-flag'; name: string }>; }); +const InternalAppContext = createContext<{ + routeObjects: BackstageRouteObject[]; +}>({ routeObjects: [] }); + /** * Get the app base path from the configured app baseUrl. * @@ -205,20 +210,12 @@ export class AppManager implements BackstageApp { [], ); - const { - routePaths, - routeParents, - routeObjects, - featureFlags, - routeBindings, - } = useMemo(() => { + const { routing, featureFlags, routeBindings } = useMemo(() => { const result = traverseElementTree({ root: children, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, + routing: routingV1Collector, collectedPlugins: pluginCollector, featureFlags: featureFlagCollector, }, @@ -241,7 +238,7 @@ export class AppManager implements BackstageApp { if (!routesHaveBeenValidated) { routesHaveBeenValidated = true; - validateRouteParameters(routePaths, routeParents); + validateRouteParameters(routing.paths, routing.parents); validateRouteBindings( routeBindings, this.plugins as Iterable>, @@ -304,13 +301,17 @@ export class AppManager implements BackstageApp { - {children} + + {children} + @@ -345,6 +346,7 @@ export class AppManager implements BackstageApp { const AppRouter = ({ children }: PropsWithChildren<{}>) => { const configApi = useApi(configApiRef); const mountPath = `${getBasePath(configApi)}/*`; + const { routeObjects } = useContext(InternalAppContext); // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { @@ -370,7 +372,7 @@ export class AppManager implements BackstageApp { return ( - + {children}} /> @@ -380,7 +382,7 @@ export class AppManager implements BackstageApp { return ( - + {children}} /> diff --git a/packages/core-app-api/src/routing/RouteTracker.tsx b/packages/core-app-api/src/routing/RouteTracker.tsx index b745dd9235..f9d8c78fc0 100644 --- a/packages/core-app-api/src/routing/RouteTracker.tsx +++ b/packages/core-app-api/src/routing/RouteTracker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { matchRoutes, useLocation } from 'react-router-dom'; import { useAnalytics, @@ -22,12 +22,6 @@ import { CommonAnalyticsContext, RouteRef, } from '@backstage/core-plugin-api'; -import { routeObjectCollector } from './collectors'; -import { - childDiscoverer, - routeElementDiscoverer, - traverseElementTree, -} from '../extensions/traversal'; import { BackstageRouteObject } from './types'; /** @@ -97,19 +91,12 @@ const TrackNavigation = ({ * Logs a "navigate" event with appropriate plugin-level analytics context * attributes each time the user navigates to a page. */ -export const RouteTracker = ({ tree }: { tree: React.ReactNode }) => { +export const RouteTracker = ({ + routeObjects, +}: { + routeObjects: BackstageRouteObject[]; +}) => { const { pathname, search, hash } = useLocation(); - // todo(iamEAP): Work this into the existing traversal and make the data - // available on the provider. Then grab from app instance on the router. - const { routeObjects } = useMemo(() => { - return traverseElementTree({ - root: tree, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routeObjects: routeObjectCollector, - }, - }); - }, [tree]); return ( diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx index 3391bab8b0..dec92072fc 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx @@ -34,11 +34,7 @@ import { ExternalRouteRef, } from '@backstage/core-plugin-api'; import { RoutingProvider } from './RoutingProvider'; -import { - routePathCollector, - routeParentCollector, - routeObjectCollector, -} from './collectors'; +import { routingV1Collector } from './collectors'; import { validateRouteParameters } from './validation'; import { RouteResolver } from './RouteResolver'; import { AnyRouteRef, RouteFunc } from './types'; @@ -135,21 +131,19 @@ function withRoutingProvider( root: ReactElement, routeBindings: [ExternalRouteRef, RouteRef][] = [], ) { - const { routePaths, routeParents, routeObjects } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, + routing: routingV1Collector, }, }); return ( @@ -314,18 +308,17 @@ describe('discovery', () => { ); - const { routePaths, routeParents } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, + routing: routingV1Collector, }, }); - expect(() => validateRouteParameters(routePaths, routeParents)).toThrow( - 'Parameter :id is duplicated in path /foo/:id/bar/:id', - ); + expect(() => + validateRouteParameters(routing.paths, routing.parents), + ).toThrow('Parameter :id is duplicated in path /foo/:id/bar/:id'); }); }); diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx index 5fb1795f4c..1c5bc8c639 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.test.tsx @@ -15,11 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; -import { - routePathCollector, - routeParentCollector, - routeObjectCollector, -} from './collectors'; +import { routingV1Collector } from './collectors'; import { traverseElementTree, @@ -162,30 +158,28 @@ describe('discovery', () => { ); - const { routes, routeParents, routeObjects } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, + routing: routingV1Collector, }, }); - expect(sortedEntries(routes)).toEqual([ + expect(sortedEntries(routing.paths)).toEqual([ [ref1, '/foo'], [ref2, '/bar/:id'], [ref3, '/baz'], [ref4, '/divsoup'], [ref5, '/blop'], ]); - expect(sortedEntries(routeParents)).toEqual([ + expect(sortedEntries(routing.parents)).toEqual([ [ref1, undefined], [ref2, ref1], [ref3, ref2], [ref4, undefined], [ref5, ref1], ]); - expect(routeObjects).toEqual([ + expect(routing.objects).toEqual([ routeObj( '/foo', [ref1], @@ -220,22 +214,21 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, + routing: routingV1Collector, }, }); - expect(sortedEntries(routes)).toEqual([ + expect(sortedEntries(routing.paths)).toEqual([ [ref1, '/foo'], [ref2, '/bar/:id'], [ref3, '/baz'], [ref4, '/divsoup'], [ref5, '/blop'], ]); - expect(sortedEntries(routeParents)).toEqual([ + expect(sortedEntries(routing.parents)).toEqual([ [ref1, undefined], [ref2, ref1], [ref3, undefined], @@ -266,30 +259,28 @@ describe('discovery', () => { ); - const { routes, routeParents, routeObjects } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, + routing: routingV1Collector, }, }); - expect(sortedEntries(routes)).toEqual([ + expect(sortedEntries(routing.paths)).toEqual([ [ref1, '/foo'], [ref2, '/foo'], [ref3, '/bar'], [ref4, '/baz'], [ref5, '/baz'], ]); - expect(sortedEntries(routeParents)).toEqual([ + expect(sortedEntries(routing.parents)).toEqual([ [ref1, undefined], [ref2, undefined], [ref3, undefined], [ref4, ref3], [ref5, ref3], ]); - expect(routeObjects).toEqual([ + expect(routing.objects).toEqual([ routeObj('/foo', [ref1, ref2], [], 'gathered'), routeObj( '/bar', @@ -317,30 +308,28 @@ describe('discovery', () => { ); - const { routes, routeParents, routeObjects } = traverseElementTree({ + const { routing } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, + routing: routingV1Collector, }, }); - expect(sortedEntries(routes)).toEqual([ + expect(sortedEntries(routing.paths)).toEqual([ [ref1, '/foo'], [ref2, '/bar'], [ref3, '/baz'], [ref4, '/blop'], [ref5, '/bar'], ]); - expect(sortedEntries(routeParents)).toEqual([ + expect(sortedEntries(routing.parents)).toEqual([ [ref1, undefined], [ref2, ref1], [ref3, ref1], [ref4, ref3], [ref5, ref1], ]); - expect(routeObjects).toEqual([ + expect(routing.objects).toEqual([ routeObj( '/foo', [ref1], @@ -376,8 +365,7 @@ describe('discovery', () => { root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routePathCollector, - routeParents: routeParentCollector, + routing: routingV1Collector, }, }); }).toThrow('Mounted routable extension must have a path'); diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx index 46734707a6..fcd954201a 100644 --- a/packages/core-app-api/src/routing/collectors.tsx +++ b/packages/core-app-api/src/routing/collectors.tsx @@ -14,106 +14,16 @@ * limitations under the License. */ -import { isValidElement, ReactElement, ReactNode } from 'react'; import { RouteRef, getComponentData, BackstagePlugin, } from '@backstage/core-plugin-api'; +import { isValidElement } from 'react'; import { BackstageRouteObject } from './types'; import { createCollector } from '../extensions/traversal'; import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged'; -function getMountPoint(node: ReactElement): RouteRef | undefined { - const element: ReactNode = node.props?.element; - - let routeRef = getComponentData(node, 'core.mountPoint'); - if (!routeRef && isValidElement(element)) { - routeRef = getComponentData(element, 'core.mountPoint'); - } - - return routeRef; -} - -export const routePathCollector = createCollector( - () => new Map(), - (acc, node, parent, ctxPath: string | undefined) => { - // The context path is used during mount point gathering to assign the same path - // to all discovered mount points - let currentCtxPath = ctxPath; - - if (parent?.props.element === node) { - return currentCtxPath; - } - - // Start gathering mount points when we encounter a mount point gathering flag - if (getComponentData(node, 'core.gatherMountPoints')) { - const path: string | undefined = node.props?.path; - if (!path) { - throw new Error('Mount point gatherer must have a path'); - } - currentCtxPath = path; - } - - const routeRef = getMountPoint(node); - if (routeRef) { - let path: string | undefined = node.props?.path; - // If we're gathering mount points we use the context path as out path, unless - // the element has its own path, in which case we use that instead and stop gathering - if (currentCtxPath) { - if (path) { - currentCtxPath = undefined; - } else { - path = currentCtxPath; - } - } - if (!path) { - throw new Error('Mounted routable extension must have a path'); - } - acc.set(routeRef, path); - } - return currentCtxPath; - }, -); - -export const routeParentCollector = createCollector( - () => new Map(), - (acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => { - if (parent?.props.element === node) { - return parentRouteRef; - } - - let nextParent = parentRouteRef; - - const routeRef = getMountPoint(node); - if (routeRef) { - // "sticky" route ref is when we've encountered a mount point gatherer, and we want a - // mount points beneath it to have the same parent, regardless of internal structure - if (parentRouteRef && 'sticky' in parentRouteRef) { - acc.set(routeRef, parentRouteRef.sticky); - - // When we encounter a mount point with an explicit path, we stop gathering - // mount points within the children and remove the sticky state - if (node.props?.path) { - nextParent = routeRef; - } else { - nextParent = parentRouteRef; - } - } else { - acc.set(routeRef, parentRouteRef); - nextParent = routeRef; - } - } - - // Mount point gatherers are marked as "sticky" - if (getComponentData(node, 'core.gatherMountPoints')) { - return { sticky: nextParent }; - } - - return nextParent; - }, -); - // We always add a child that matches all subroutes but without any route refs. This makes // sure that we're always able to match each route no matter how deep the navigation goes. // The route resolver then takes care of selecting the most specific match in order to find @@ -125,21 +35,107 @@ export const MATCH_ALL_ROUTE: BackstageRouteObject = { routeRefs: new Set(), }; -export const routeObjectCollector = createCollector( - () => Array(), - (acc, node, parent, parentObj: BackstageRouteObject | undefined) => { - const parentChildren = parentObj?.children ?? acc; +interface RoutingV1CollectorContext { + path?: string; + routeRef?: RouteRef; + obj?: BackstageRouteObject; + sticky?: boolean; +} + +/** + * This is the old V1 logic for collecting the routing model. + * It is being replaced by a new collector because this collection + * logic does not work well beyond react-router v6 beta. + * + * The breaking change is that react-router now requires route + * elements to be `Route` components, and directly renders the + * element prop rather than the `Route` itself. This means it is + * no longer possible to create utility route components. In order + * to fill this gap and in general simplify the route collection + * logic, a new route collection logic is created. + * + * @internal + */ +export const routingV1Collector = createCollector( + () => ({ + paths: new Map(), + parents: new Map(), + objects: new Array(), + }), + (acc, node, parent, ctx?: RoutingV1CollectorContext) => { + // Ignore the top-level element within element props, since it's already been collected. if (parent?.props.element === node) { - return parentObj; + return ctx; } + let currentObj = ctx?.obj; + let currentParentRouteRef = ctx?.routeRef; + let sticky = ctx?.sticky; + const path: string | undefined = node.props?.path; + const parentChildren = currentObj?.children ?? acc.objects; const caseSensitive: boolean = Boolean(node.props?.caseSensitive); - const routeRef = getMountPoint(node); + // The context path is used during mount point gathering to assign the same path + // to all discovered mount points + let currentCtxPath = ctx?.path; + + // Start gathering mount points when we encounter a mount point gathering flag + if (getComponentData(node, 'core.gatherMountPoints')) { + if (!path) { + throw new Error('Mount point gatherer must have a path'); + } + currentCtxPath = path; + } + + // Route refs are discovered on the element itself, and on the top-level + // element within the element prop if it exists. + const element = node.props?.element; + let routeRef = getComponentData(node, 'core.mountPoint'); + if (!routeRef && isValidElement(element)) { + routeRef = getComponentData(element, 'core.mountPoint'); + } + if (routeRef) { + // First the path gathering + + let routePath: string | undefined = path; + // If we're gathering mount points we use the context path as out path, unless + // the element has its own path, in which case we use that instead and stop gathering + if (currentCtxPath) { + if (routePath) { + currentCtxPath = undefined; + } else { + routePath = currentCtxPath; + } + } + if (!routePath) { + throw new Error('Mounted routable extension must have a path'); + } + acc.paths.set(routeRef, routePath); + + // Then the parent gathering + + // "sticky" route ref is when we've encountered a mount point gatherer, and we want a + // mount points beneath it to have the same parent, regardless of internal structure + if (currentParentRouteRef && sticky) { + acc.parents.set(routeRef, currentParentRouteRef); + + // When we encounter a mount point with an explicit path, we stop gathering + // mount points within the children and remove the sticky state + if (path) { + currentParentRouteRef = routeRef; + sticky = false; + } + } else { + acc.parents.set(routeRef, currentParentRouteRef); + currentParentRouteRef = routeRef; + } + + // Then construct the objects + if (path) { - const newObject: BackstageRouteObject = { + currentObj = { caseSensitive, path, element: 'mounted', @@ -150,11 +146,14 @@ export const routeObjectCollector = createCollector( 'core.plugin', ), }; - parentChildren.push(newObject); - return newObject; + parentChildren.push(currentObj); + } else { + currentObj?.routeRefs.add(routeRef); } + } - parentObj?.routeRefs.add(routeRef); + if (getComponentData(node, 'core.gatherMountPoints')) { + sticky = true; } const isGatherer = getComponentData( @@ -165,19 +164,25 @@ export const routeObjectCollector = createCollector( if (!path) { throw new Error('Mount point gatherer must have a path'); } - const newObject: BackstageRouteObject = { - caseSensitive, - path, - element: 'gathered', - routeRefs: new Set(), - children: [MATCH_ALL_ROUTE], - plugin: parentObj?.plugin, - }; - parentChildren.push(newObject); - return newObject; + if (!routeRef) { + currentObj = { + caseSensitive, + path, + element: 'gathered', + routeRefs: new Set(), + children: [MATCH_ALL_ROUTE], + plugin: ctx?.obj?.plugin, + }; + parentChildren.push(currentObj); + } } - return parentObj; + return { + obj: currentObj, + path: currentCtxPath, + routeRef: currentParentRouteRef, + sticky, + }; }, ); diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index fe4ed03142..dc246eaa52 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/core-components +## 0.9.3 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 25b8e8d5b5: Add BackstageTab to overridableComponents so can override styles in a theme +- 230ad0826f: Bump to using `@types/node` v16 +- 41fd107189: Exported `IdentityProviders` type. +- a13604b8f7: Adding a name to the core-components Tab styles so can customise in the theme settings +- 19648d5cf5: fix support config ref to use backstage/backstage +- d505e43ffc: Fix highlighting of active sidebar items. +- 72f3dfd05a: Updated ProxiedSignInPageProps docs +- 7741e47eae: `` now accepts additional props `sidebarOptions` and `submenuOptions` to allow further customization +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + +## 0.9.3-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 41fd107189: Exported `IdentityProviders` type. +- d505e43ffc: Fix highlighting of active sidebar items. +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.9.3-next.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index d7e8be8d8e..8060ddfc8d 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.9.3-next.1", + "version": "0.9.3", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/theme": "^0.2.15", "@material-table/core": "^3.1.0", @@ -78,12 +78,12 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/cli": "^0.17.0-next.2", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/core-app-api": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/classnames": "^2.2.9", "@types/d3-selection": "^3.0.1", diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index b3beed9147..4a0376946a 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -37,6 +37,7 @@ import React, { KeyboardEventHandler, ReactNode, useContext, + useMemo, useState, } from 'react'; import { @@ -83,122 +84,132 @@ export type SidebarItemClassKey = | 'arrows' | 'selected'; -const useStyles = makeStyles( - theme => ({ - root: { - color: theme.palette.navigation.color, - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - height: 48, - cursor: 'pointer', - }, - buttonItem: { - background: 'none', - border: 'none', - width: '100%', - margin: 0, - padding: 0, - textAlign: 'inherit', - font: 'inherit', - }, - closed: props => ({ - width: props.sidebarConfig.drawerWidthClosed, - justifyContent: 'center', - }), - open: props => ({ - [theme.breakpoints.up('sm')]: { - width: props.sidebarConfig.drawerWidthOpen, +const makeSidebarStyles = (sidebarConfig: SidebarConfig) => + makeStyles( + theme => ({ + root: { + color: theme.palette.navigation.color, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + height: 48, + cursor: 'pointer', }, - }), - highlightable: { - '&:hover': { + buttonItem: { + background: 'none', + border: 'none', + width: '100%', + margin: 0, + padding: 0, + textAlign: 'inherit', + font: 'inherit', + }, + closed: { + width: sidebarConfig.drawerWidthClosed, + justifyContent: 'center', + }, + open: { + [theme.breakpoints.up('sm')]: { + width: sidebarConfig.drawerWidthOpen, + }, + }, + highlightable: { + '&:hover': { + background: + theme.palette.navigation.navItem?.hoverBackground ?? '#404040', + }, + }, + highlighted: { background: theme.palette.navigation.navItem?.hoverBackground ?? '#404040', }, - }, - highlighted: { - background: - theme.palette.navigation.navItem?.hoverBackground ?? '#404040', - }, - label: { - // XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs - fontWeight: 'bold', - whiteSpace: 'nowrap', - lineHeight: 'auto', - flex: '3 1 auto', - width: '110px', - overflow: 'hidden', - 'text-overflow': 'ellipsis', - }, - iconContainer: props => ({ - boxSizing: 'border-box', - height: '100%', - width: props.sidebarConfig.iconContainerWidth, - marginRight: -theme.spacing(2), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }), - searchRoot: { - marginBottom: 12, - }, - searchField: { - color: '#b5b5b5', - fontWeight: 'bold', - fontSize: theme.typography.fontSize, - }, - searchFieldHTMLInput: { - padding: theme.spacing(2, 0, 2), - }, - searchContainer: props => ({ - width: - props.sidebarConfig.drawerWidthOpen - - props.sidebarConfig.iconContainerWidth, - }), - secondaryAction: { - width: theme.spacing(6), - textAlign: 'center', - marginRight: theme.spacing(1), - }, - closedItemIcon: { - width: '100%', - justifyContent: 'center', - }, - submenuArrow: { - display: 'flex', - }, - expandButton: { - background: 'none', - border: 'none', - color: theme.palette.navigation.color, - width: '100%', - cursor: 'pointer', - position: 'relative', - height: 48, - }, - arrows: { - position: 'absolute', - right: 10, - }, - selected: props => ({ - '&$root': { - borderLeft: `solid ${props.sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, - color: theme.palette.navigation.selectedColor, + label: { + // XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs + fontWeight: 'bold', + whiteSpace: 'nowrap', + lineHeight: 'auto', + flex: '3 1 auto', + width: '110px', + overflow: 'hidden', + 'text-overflow': 'ellipsis', }, - '&$closed': { - width: props.sidebarConfig.drawerWidthClosed, + iconContainer: { + boxSizing: 'border-box', + height: '100%', + width: sidebarConfig.iconContainerWidth, + marginRight: -theme.spacing(2), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }, - '& $closedItemIcon': { - paddingRight: props.sidebarConfig.selectedIndicatorWidth, + searchRoot: { + marginBottom: 12, }, - '& $iconContainer': { - marginLeft: -props.sidebarConfig.selectedIndicatorWidth, + searchField: { + color: '#b5b5b5', + fontWeight: 'bold', + fontSize: theme.typography.fontSize, + }, + searchFieldHTMLInput: { + padding: theme.spacing(2, 0, 2), + }, + searchContainer: { + width: sidebarConfig.drawerWidthOpen - sidebarConfig.iconContainerWidth, + }, + secondaryAction: { + width: theme.spacing(6), + textAlign: 'center', + marginRight: theme.spacing(1), + }, + closedItemIcon: { + width: '100%', + justifyContent: 'center', + }, + submenuArrow: { + display: 'flex', + }, + expandButton: { + background: 'none', + border: 'none', + color: theme.palette.navigation.color, + width: '100%', + cursor: 'pointer', + position: 'relative', + height: 48, + }, + arrows: { + position: 'absolute', + right: 10, + }, + selected: { + '&$root': { + borderLeft: `solid ${sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, + color: theme.palette.navigation.selectedColor, + }, + '&$closed': { + width: sidebarConfig.drawerWidthClosed, + }, + '& $closedItemIcon': { + paddingRight: sidebarConfig.selectedIndicatorWidth, + }, + '& $iconContainer': { + marginLeft: -sidebarConfig.selectedIndicatorWidth, + }, }, }), - }), - { name: 'BackstageSidebarItem' }, -); + { name: 'BackstageSidebarItem' }, + ); + +// This is a workaround for this issue https://github.com/mui/material-ui/issues/15511 +// The styling of the `selected` elements doesn't work as expected when using a prop callback. +// Don't use this pattern unless needed +function useMemoStyles(sidebarConfig: SidebarConfig) { + const useStyles = useMemo( + () => makeSidebarStyles(sidebarConfig), + [sidebarConfig], + ); + return useStyles(); +} /** * Evaluates the routes of the SubmenuItems & nested DropdownItems. @@ -352,7 +363,7 @@ const SidebarItemBase = forwardRef((props, ref) => { ...navLinkProps } = props; const { sidebarConfig } = useContext(SidebarConfigContext); - const classes = useStyles({ sidebarConfig }); + const classes = useMemoStyles(sidebarConfig); // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component // depend on the current location, and at least have it being optionally forced to selected. // Still waiting on a Q answered to fine tune the implementation @@ -426,7 +437,7 @@ const SidebarItemWithSubmenu = ({ children: React.ReactElement; }) => { const { sidebarConfig } = useContext(SidebarConfigContext); - const classes = useStyles({ sidebarConfig }); + const classes = useMemoStyles(sidebarConfig); const [isHoveredOn, setIsHoveredOn] = useState(false); const location = useLocation(); const isActive = useLocationMatch(children, location); @@ -517,7 +528,7 @@ type SidebarSearchFieldProps = { export function SidebarSearchField(props: SidebarSearchFieldProps) { const { sidebarConfig } = useContext(SidebarConfigContext); const [input, setInput] = useState(''); - const classes = useStyles({ sidebarConfig }); + const classes = useMemoStyles(sidebarConfig); const Icon = props.icon ? props.icon : SearchIcon; const search = () => { @@ -646,7 +657,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => { */ export const SidebarExpandButton = () => { const { sidebarConfig } = useContext(SidebarConfigContext); - const classes = useStyles({ sidebarConfig }); + const classes = useMemoStyles(sidebarConfig); const { isOpen, setOpen } = useContext(SidebarContext); const isSmallScreen = useMediaQuery( theme => theme.breakpoints.down('md'), diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 907645a3ba..d4ad5dc787 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/core-plugin-api +## 1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/version-bridge@1.0.1 + +## 1.0.1-next.0 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 + ## 1.0.0 ### Major Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 89361419ba..8289e863e0 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -172,7 +172,7 @@ export type AppThemeApi = { // @public export const appThemeApiRef: ApiRef; -// @alpha +// @public export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; @@ -230,7 +230,7 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; -// @alpha +// @public export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; @@ -462,17 +462,17 @@ export function getComponentData( type: string, ): T | undefined; -// @alpha +// @public export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// @alpha +// @public export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// @alpha +// @public export const googleAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -517,7 +517,7 @@ export type MergeParams< P2 extends AnyParams, > = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); -// @alpha +// @public export const microsoftAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -559,7 +559,7 @@ export type OAuthRequesterOptions = { // @public export type OAuthScope = string | string[]; -// @alpha +// @public export const oktaAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -568,7 +568,7 @@ export const oktaAuthApiRef: ApiRef< SessionApi >; -// @alpha +// @public export const oneloginAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 75e344b5ae..c3c5a9685d 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.0.0", + "version": "1.0.1", "private": false, "publishConfig": { "access": "public", @@ -35,7 +35,7 @@ "dependencies": { "@backstage/config": "^1.0.0", "@backstage/types": "^1.0.0", - "@backstage/version-bridge": "^1.0.0", + "@backstage/version-bridge": "^1.0.1", "history": "^5.0.0", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -46,12 +46,12 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 497ce8233b..163ae74806 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -295,8 +295,7 @@ export type SessionApi = { /** * Provides authentication towards Google APIs and identities. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. - * + * @public * @remarks * * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. @@ -317,8 +316,7 @@ export const googleAuthApiRef: ApiRef< /** * Provides authentication towards GitHub APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. - * + * @public * @remarks * * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} @@ -333,8 +331,7 @@ export const githubAuthApiRef: ApiRef< /** * Provides authentication towards Okta APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. - * + * @public * @remarks * * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} @@ -353,8 +350,7 @@ export const oktaAuthApiRef: ApiRef< /** * Provides authentication towards GitLab APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. - * + * @public * @remarks * * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} @@ -369,8 +365,7 @@ export const gitlabAuthApiRef: ApiRef< /** * Provides authentication towards Microsoft APIs and identities. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. - * + * @public * @remarks * * For more info and a full list of supported scopes, see: @@ -390,7 +385,7 @@ export const microsoftAuthApiRef: ApiRef< /** * Provides authentication towards OneLogin APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. + * @public */ export const oneloginAuthApiRef: ApiRef< OAuthApi & @@ -405,7 +400,7 @@ export const oneloginAuthApiRef: ApiRef< /** * Provides authentication towards Bitbucket APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. + * @public * @remarks * * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} @@ -420,7 +415,7 @@ export const bitbucketAuthApiRef: ApiRef< /** * Provides authentication towards Atlassian APIs. * - * @alpha This API is **EXPERIMENTAL** and might change in the future. + * @public * @remarks * * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 31376eb4ff..34780069e9 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,410 @@ # @backstage/create-app +## 0.4.27-next.0 + +### Patch Changes + +- 3983940a21: Optimized the command order in `packages/backend/Dockerfile` as well as added the `--no-install-recommends` to the `apt-get install` and tweaked the installed packages. + + To apply this change to an existing app, update your `packages/backend/Dockerfile` to match the documented `Dockerfile` at https://backstage.io/docs/deployment/docker#host-build. + +- 28bbf5aff6: Added some instruction comments to the generated config files, to clarify the + usage of `backend.baseUrl` and `backend.listen.host`. Importantly, it also per + default now listens on all IPv4 interfaces, to make it easier to take the step + over to production. If you want to do the same, update your + `app-config.production.yaml` as follows: + + ```diff + backend: + listen: + port: 7007 + + host: 0.0.0.0 + ``` + + Also, updated the builtin backend Dockerfile to honor the + `app-config.production.yaml` file. If you want to do the same, change + `packages/backend/Dockerfile` as follows: + + ```diff + -COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./ + +COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ + RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + + -CMD ["node", "packages/backend", "--config", "app-config.yaml"] + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] + ``` + + If you look carefully, this adds a glob match on app-config files. For those + that try out the build flows locally, you also want to make sure that the docker + daemon does NOT pick up any local/private config files that might contain + secrets. You should therefore also update your local `.dockerignore` file at the + same time: + + ```diff + +*.local.yaml + ``` + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 344ea56acc: Bump `commander` to version 9.1.0 +- 806427545f: Added a link to the `${GITHUB_TOKEN}` to document how to generate a token + +## 0.4.26 + +### Patch Changes + +- 1691c6c5c2: Made `User` and `Group` entity kinds not permitted by the default + `catalog.rules` config. + + The effect of this is that after creating a new Backstage repository, its + catalog no longer permits regular users to register `User` or `Group` entities + using the Backstage interface. Additionally, if you have config locations that + result in `User` or `Group` entities, you need to add those kinds to its own + specific rules: + + ```yaml + catalog: + locations: + # This applies for example to url type locations + - type: url + target: https://example.com/org.yaml + rules: + - allow: [User, Group] + # But also note that this applies to ALL org location types! + - type: github-org + target: https://github.com/my-org-name + rules: + - allow: [User, Group] + ``` + + This rule change does NOT affect entity providers, only things that are emitted + by entity processors. + + We recommend that this change is applied to your own Backstage repository, since + it makes it impossible for regular end users to affect your org data through + e.g. YAML files. To do so, remove the two kinds from the default rules in your config: + + ```diff + catalog: + rules: + - - allow: [Component, System, API, Group, User, Resource, Location] + + - allow: [Component, System, API, Resource, Location] + ``` + + And for any location that in any way results in org data being ingested, add the corresponding rule to it: + + ```diff + catalog: + locations: + - type: github-org + target: https://github.com/my-org-name + + rules: + + - allow: [User, Group] + ``` + +- 0e911394d2: Remove the `knex` package that is installed in the `packages/backend` as it's provided by the `@backstage/*` packages for you automatically. You can make the following change in your `packages/backend/package.json` if you wish to apply this change. + + ```diff + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + - "migrate:create": "knex migrate:make -x ts" + ``` + + ```diff + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + - "knex": "^0.21.6", + "pg": "^8.3.0", + ``` + +- 520e21aaea: imports `useSearch` hook from new `@backstage/plugin-search-react` package. + + To upgrade existing Apps: + + 1. Change the import to the following: + + `packages/app/src/components/search/SearchPage.tsx` + + ```diff + import { + ... + SearchType, + - useSearch, + } from '@backstage/plugin-search'; + +import { useSearch } from '@backstage/plugin-search-react'; + ``` + + 2. Add `@backstage/plugin-search-react` as a dependency to the app. + +- 43759dd789: Removed `@octokit/rest` and `@gitbeaker/node` from backend dependencies as these are unused in the default app. + + To apply these changes to your existing app, remove the following lines from the `dependencies` section of `packages/backend/package.json` + + ```diff + "@backstage/plugin-techdocs-backend": "^1.0.0", + - "@gitbeaker/node": "^34.6.0", + - "@octokit/rest": "^18.5.3", + ``` + +- e838a7060a: Add type resolutions for `@types/react` and `types/react-dom`. + + The reason for this is the usage of `"@types/react": "*"` as a dependency which is very common practice in react packages. This recently resolves to react 18 which introduces several breaking changes in both internal and external packages. + + To apply these changes to your existing installation, add a resolutions block to your `package.json` + + ```json + "resolutions": { + "@types/react": "^17", + "@types/react-dom": "^17" + }, + ``` + + If your existing app depends on react 16, use this resolution block instead. + + ```json + "resolutions": { + "@types/react": "^16", + "@types/react-dom": "^16" + }, + ``` + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +- c07d9f9e1c: Add helpful README.md files in the original `packages` and `plugins` folders +- 230ad0826f: Bump to using `@types/node` v16 +- 1882dbda2b: Accept `PermissionEvaluator` instead of the deprecated `PermissionAuthorizer`. + + Apply the following to `packages/backend/src/types.ts`: + + ```diff + - import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; + + import { PermissionEvaluator } from '@backstage/plugin-permission-common'; + + export type PluginEnvironment = { + ... + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + scheduler: PluginTaskScheduler; + - permissions: PermissionAuthorizer; + + permissions: PermissionEvaluator; + }; + ``` + +- e80cca164d: Tweaked `.eslintrc.js` files in the template to avoid having them apply during development. This change does not affect create apps. + +## 0.4.26-next.2 + +### Patch Changes + +- 43759dd789: Removed `@octokit/rest` and `@gitbeaker/node` from backend dependencies as these are unused in the default app. + + To apply these changes to your existing app, remove the following lines from the `dependencies` section of `packages/backend/package.json` + + ```diff + "@backstage/plugin-techdocs-backend": "^1.0.0", + - "@gitbeaker/node": "^34.6.0", + - "@octokit/rest": "^18.5.3", + ``` + +- e838a7060a: Add type resolutions for `@types/react` and `types/react-dom`. + + The reason for this is the usage of `"@types/react": "*"` as a dependency which is very common practice in react packages. This recently resolves to react 18 which introduces several breaking changes in both internal and external packages. + + To apply these changes to your existing installation, add a resolutions block to your `package.json` + + ```json + "resolutions": { + "@types/react": "^17", + "@types/react-dom": "^17" + }, + ``` + + If your existing app depends on react 16, use this resolution block instead. + + ```json + "resolutions": { + "@types/react": "^16", + "@types/react-dom": "^16" + }, + ``` + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +- 230ad0826f: Bump to using `@types/node` v16 +- 1882dbda2b: Accept `PermissionEvaluator` instead of the deprecated `PermissionAuthorizer`. + + Apply the following to `packages/backend/src/types.ts`: + + ```diff + - import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; + + import { PermissionEvaluator } from '@backstage/plugin-permission-common'; + + export type PluginEnvironment = { + ... + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + scheduler: PluginTaskScheduler; + - permissions: PermissionAuthorizer; + + permissions: PermissionEvaluator; + }; + ``` + ## 0.4.25-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 502409b8e1..94641115fe 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.25-next.1", + "version": "0.4.27-next.0", "private": false, "publishConfig": { "access": "public" @@ -35,8 +35,8 @@ "dependencies": { "@backstage/cli-common": "^0.1.8", "chalk": "^4.0.0", - "commander": "^6.1.0", - "fs-extra": "10.0.1", + "commander": "^9.1.0", + "fs-extra": "10.1.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", "ora": "^5.3.0", @@ -85,6 +85,7 @@ "@backstage/plugin-scaffolder": "", "@backstage/plugin-scaffolder-backend": "", "@backstage/plugin-search": "", + "@backstage/plugin-search-react": "", "@backstage/plugin-search-backend": "", "@backstage/plugin-search-backend-module-pg": "", "@backstage/plugin-search-backend-node": "", diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 36143a4172..83584445b0 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -15,7 +15,7 @@ */ import chalk from 'chalk'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import inquirer, { Answers } from 'inquirer'; import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; @@ -30,7 +30,7 @@ import { templatingTask, } from './lib/tasks'; -export default async (cmd: Command): Promise => { +export default async (opts: OptionValues): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -65,22 +65,22 @@ export default async (cmd: Command): Promise => { // Use `--path` argument as application directory when specified, otherwise // create a directory using `answers.name` - const appDir = cmd.path - ? resolvePath(paths.targetDir, cmd.path) + const appDir = opts.path + ? resolvePath(paths.targetDir, opts.path) : resolvePath(paths.targetDir, answers.name); Task.log(); Task.log('Creating the app...'); try { - if (cmd.path) { + if (opts.path) { // Template directly to specified path Task.section('Checking that supplied path exists'); await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, cmd.path, answers); + await templatingTask(templateDir, opts.path, answers); } else { // Template to temporary location, and then move files @@ -97,7 +97,7 @@ export default async (cmd: Command): Promise => { await moveAppTask(tempDir, appDir, answers.name); } - if (!cmd.skipInstall) { + if (!opts.skipInstall) { Task.section('Building the app'); await buildAppTask(appDir); } diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index a8ff3dc9b9..adb8560f31 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -20,7 +20,7 @@ * @packageDocumentation */ -import program from 'commander'; +import { program } from 'commander'; import { exitWithError } from './lib/errors'; import { version } from '../package.json'; import createApp from './createApp'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 963c04afdf..b6c93a38b3 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -69,6 +69,7 @@ import { version as pluginRollbarBackend } from '../../../../plugins/rollbar-bac import { version as pluginScaffolder } from '../../../../plugins/scaffolder/package.json'; import { version as pluginScaffolderBackend } from '../../../../plugins/scaffolder-backend/package.json'; import { version as pluginSearch } from '../../../../plugins/search/package.json'; +import { version as pluginSearchReact } from '../../../../plugins/search-react/package.json'; import { version as pluginSearchBackend } from '../../../../plugins/search-backend/package.json'; import { version as pluginSearchBackendModulePg } from '../../../../plugins/search-backend-module-pg/package.json'; import { version as pluginSearchBackendNode } from '../../../../plugins/search-backend-node/package.json'; @@ -113,6 +114,7 @@ export const packageVersions = { '@backstage/plugin-scaffolder': pluginScaffolder, '@backstage/plugin-scaffolder-backend': pluginScaffolderBackend, '@backstage/plugin-search': pluginSearch, + '@backstage/plugin-search-react': pluginSearchReact, '@backstage/plugin-search-backend': pluginSearchBackend, '@backstage/plugin-search-backend-module-pg': pluginSearchBackendModulePg, '@backstage/plugin-search-backend-node': pluginSearchBackendNode, diff --git a/packages/create-app/templates/default-app/.dockerignore b/packages/create-app/templates/default-app/.dockerignore index 63c9c34286..5c3b1360e0 100644 --- a/packages/create-app/templates/default-app/.dockerignore +++ b/packages/create-app/templates/default-app/.dockerignore @@ -3,3 +3,4 @@ node_modules packages !packages/backend/dist plugins +*.local.yaml diff --git a/packages/create-app/templates/default-app/app-config.production.yaml b/packages/create-app/templates/default-app/app-config.production.yaml index 5e36c2319f..c180bbfdec 100644 --- a/packages/create-app/templates/default-app/app-config.production.yaml +++ b/packages/create-app/templates/default-app/app-config.production.yaml @@ -1,8 +1,19 @@ app: - # Should be the same as backend.baseUrl when using the `app-backend` plugin + # Should be the same as backend.baseUrl when using the `app-backend` plugin. baseUrl: http://localhost:7007 backend: + # Note that the baseUrl should be the URL that the browser and other clients + # should use when communicating with the backend, i.e. it needs to be + # reachable not just from within the backend host, but from all of your + # callers. When its value is "http://localhost:7007", it's strictly private + # and can't be reached by others. baseUrl: http://localhost:7007 listen: port: 7007 + # The following host directive binds to all IPv4 interfaces when its value + # is "0.0.0.0". This is the most permissive setting. The right value depends + # on your specific deployment. If you remove the host line entirely, the + # backend will bind on the interface that corresponds to the backend.baseUrl + # hostname. + host: 0.0.0.0 diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 380d2a765d..e274ffeaf2 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -15,6 +15,9 @@ backend: baseUrl: http://localhost:7007 listen: port: 7007 + # Uncomment the following host directive to bind to all IPv4 interfaces and + # not just the baseUrl hostname. + # host: 0.0.0.0 csp: connect-src: ["'self'", 'http:', 'https:'] # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference @@ -51,6 +54,8 @@ backend: integrations: github: - host: github.com + # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information + # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration token: ${GITHUB_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: # - host: ghe.example.net diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 2ad394c68e..87a8060594 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -25,6 +25,7 @@ "@backstage/plugin-permission-react": "^{{version '@backstage/plugin-permission-react'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", "@backstage/plugin-search": "^{{version '@backstage/plugin-search'}}", + "@backstage/plugin-search-react": "^{{version '@backstage/plugin-search-react'}}", "@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}", "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", "@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index cd4603ecd3..0595e4ece8 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -14,8 +14,8 @@ import { SearchResult, SearchType, DefaultResultListItem, - useSearch, } from '@backstage/plugin-search'; +import { useSearch } from '@backstage/plugin-search-react'; import { CatalogIcon, Content, diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index dd35d4ddbc..a5773aa0f8 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -13,22 +13,22 @@ FROM node:16-bullseye-slim WORKDIR /app +# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ + rm -rf /var/lib/apt/lists/* && \ + yarn config set python /usr/bin/python3 + # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. # The skeleton contains the package.json of each package in the monorepo, # and along with yarn.lock and the root package.json, that's enough to run yarn install. COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -# install sqlite3 dependencies -RUN apt-get update && \ - apt-get install -y libsqlite3-dev python3 cmake g++ && \ - rm -rf /var/lib/apt/lists/* && \ - yarn config set python /usr/bin/python3 - RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" # Then copy the rest of the backend bundle, along with any other files we might want. -COPY packages/backend/dist/bundle.tar.gz app-config.yaml ./ +COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz -CMD ["node", "packages/backend", "--config", "app-config.yaml"] +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 9bd441f001..e0567953e0 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/dev-utils +## 1.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/test-utils@1.0.2-next.0 + +## 1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/test-utils@1.0.1 + - @backstage/app-defaults@1.0.1 + +## 1.0.1-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/test-utils@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/app-defaults@1.0.1-next.2 + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index dc875c5ab5..88e8e94aa1 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.1-next.0", + "version": "1.0.2-next.0", "private": false, "publishConfig": { "access": "public", @@ -33,14 +33,14 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/app-defaults": "^1.0.1-next.1", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/app-defaults": "^1.0.1", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -59,7 +59,7 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 95bc79b7f1..24a9daac6f 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -35,9 +35,9 @@ "@types/node": "^16.11.26", "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", - "commander": "^6.1.0", + "commander": "^9.1.0", "cross-fetch": "^3.1.5", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "handlebars": "^4.7.3", "pgtools": "^0.3.0", "puppeteer": "^13.1.1", diff --git a/packages/e2e-test/src/commands/index.ts b/packages/e2e-test/src/commands/index.ts index 51066293bf..95d311427c 100644 --- a/packages/e2e-test/src/commands/index.ts +++ b/packages/e2e-test/src/commands/index.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { CommanderStatic } from 'commander'; +import { Command } from 'commander'; import { run } from './run'; -export function registerCommands(program: CommanderStatic) { +export function registerCommands(program: Command) { program.command('run').description('Run e2e tests').action(run); } diff --git a/packages/e2e-test/src/index.ts b/packages/e2e-test/src/index.ts index a66d6bb57d..92adc24b1e 100644 --- a/packages/e2e-test/src/index.ts +++ b/packages/e2e-test/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import program from 'commander'; +import { program } from 'commander'; import chalk from 'chalk'; import { registerCommands } from './commands'; import { version } from '../package.json'; diff --git a/packages/errors/package.json b/packages/errors/package.json index aad43f6918..f37c103f37 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -38,7 +38,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index ddcd61a667..06e985a149 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/integration-react +## 1.1.0-next.0 + +### Minor Changes + +- 1b4e1e2306: Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer` + (backwards compatible). + + In order to migrate to the new integration configs, + move your configs from `integrations.bitbucket` + to `integrations.bitbucketCloud` or `integrations.bitbucketServer`. + + Migration example: + + **Before:** + + ```yaml + integrations: + bitbucket: + - host: bitbucket.org + username: bitbucket_user + appPassword: app-password + - host: bitbucket-server.company.com + token: my-token + ``` + + **After:** + + ```yaml + integrations: + bitbucketCloud: + - username: bitbucket_user + appPassword: app-password + bitbucketServer: + - host: bitbucket-server.company.com + token: my-token + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## 1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 1.0.1-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration@1.1.0-next.2 + ## 1.0.1-next.1 ### Patch Changes diff --git a/packages/integration-react/dev/DevPage.tsx b/packages/integration-react/dev/DevPage.tsx index 07afbcd660..8ff594d80d 100644 --- a/packages/integration-react/dev/DevPage.tsx +++ b/packages/integration-react/dev/DevPage.tsx @@ -53,6 +53,14 @@ export const DevPage = () => { Bitbucket + + Bitbucket Cloud + + + + Bitbucket Server + + GitHub diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index a4322b9c0e..1c3ebbb31b 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.0.1-next.1", + "version": "1.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,9 +25,9 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,9 +38,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts index aa291c2815..fb53bbdb12 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts @@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => { it('should be instantiated', () => { const i = ScmIntegrationsApi.fromConfig(new ConfigReader({})); - expect(i.list().length).toBe(5); // The default ones + expect(i.list().length).toBe(6); // The default ones }); }); diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 2b1d93ea57..ae53c872d5 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/integration +## 1.2.0-next.0 + +### Minor Changes + +- 6673babab9: Gerrit UrlReader: Implemented `readTree` +- 1b4e1e2306: Split `bitbucket` integration into `bitbucketCloud` and `bitbucketServer` + (backwards compatible). + + In order to migrate to the new integration configs, + move your configs from `integrations.bitbucket` + to `integrations.bitbucketCloud` or `integrations.bitbucketServer`. + + Migration example: + + **Before:** + + ```yaml + integrations: + bitbucket: + - host: bitbucket.org + username: bitbucket_user + appPassword: app-password + - host: bitbucket-server.company.com + token: my-token + ``` + + **After:** + + ```yaml + integrations: + bitbucketCloud: + - username: bitbucket_user + appPassword: app-password + bitbucketServer: + - host: bitbucket-server.company.com + token: my-token + ``` + +- 566407bf8a: Gerrit Integration: Added the `getGerritProjectsApiUrl` function + +## 1.1.0 + +### Minor Changes + +- b7436743cb: Gerrit integration: Added an optional configuration to set the Gitiles base url. + +### Patch Changes + +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` +- 3ef123bbf0: Support external ID when assuming roles in S3 integration + + In order to assume a role created by a 3rd party as external + ID is needed. This change adds an optional field to the s3 + integration configuration and consumes that in the AwsS3UrlReader. + +- d26e1b0146: Exported `replaceGitLabUrlType` from package + +## 1.1.0-next.2 + +### Patch Changes + +- d26e1b0146: Exported `replaceGitLabUrlType` from package + ## 1.1.0-next.1 ### Minor Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index cdb260f089..3ffb131900 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -66,6 +66,35 @@ export type AzureIntegrationConfig = { }; // @public +export class BitbucketCloudIntegration implements ScmIntegration { + constructor(integrationConfig: BitbucketCloudIntegrationConfig); + // (undocumented) + get config(): BitbucketCloudIntegrationConfig; + // (undocumented) + static factory: ScmIntegrationsFactory; + // (undocumented) + resolveEditUrl(url: string): string; + // (undocumented) + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number; + }): string; + // (undocumented) + get title(): string; + // (undocumented) + get type(): string; +} + +// @public +export type BitbucketCloudIntegrationConfig = { + host: string; + apiBaseUrl: string; + username?: string; + appPassword?: string; +}; + +// @public @deprecated export class BitbucketIntegration implements ScmIntegration { constructor(integrationConfig: BitbucketIntegrationConfig); // (undocumented) @@ -86,7 +115,7 @@ export class BitbucketIntegration implements ScmIntegration { get type(): string; } -// @public +// @public @deprecated export type BitbucketIntegrationConfig = { host: string; apiBaseUrl: string; @@ -95,6 +124,34 @@ export type BitbucketIntegrationConfig = { appPassword?: string; }; +// @public +export class BitbucketServerIntegration implements ScmIntegration { + constructor(integrationConfig: BitbucketServerIntegrationConfig); + // (undocumented) + get config(): BitbucketServerIntegrationConfig; + // (undocumented) + static factory: ScmIntegrationsFactory; + // (undocumented) + resolveEditUrl(url: string): string; + // (undocumented) + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number; + }): string; + // (undocumented) + get title(): string; + // (undocumented) + get type(): string; +} + +// @public +export type BitbucketServerIntegrationConfig = { + host: string; + apiBaseUrl: string; + token?: string; +}; + // @public export class DefaultGithubCredentialsProvider implements GithubCredentialsProvider @@ -138,6 +195,7 @@ export class GerritIntegration implements ScmIntegration { export type GerritIntegrationConfig = { host: string; baseUrl?: string; + cloneUrl?: string; gitilesBaseUrl?: string; username?: string; password?: string; @@ -161,36 +219,103 @@ export function getAzureRequestOptions( }; // @public +export function getBitbucketCloudDefaultBranch( + url: string, + config: BitbucketCloudIntegrationConfig, +): Promise; + +// @public +export function getBitbucketCloudDownloadUrl( + url: string, + config: BitbucketCloudIntegrationConfig, +): Promise; + +// @public +export function getBitbucketCloudFileFetchUrl( + url: string, + config: BitbucketCloudIntegrationConfig, +): string; + +// @public +export function getBitbucketCloudRequestOptions( + config: BitbucketCloudIntegrationConfig, +): { + headers: Record; +}; + +// @public @deprecated export function getBitbucketDefaultBranch( url: string, config: BitbucketIntegrationConfig, ): Promise; -// @public +// @public @deprecated export function getBitbucketDownloadUrl( url: string, config: BitbucketIntegrationConfig, ): Promise; -// @public +// @public @deprecated export function getBitbucketFileFetchUrl( url: string, config: BitbucketIntegrationConfig, ): string; -// @public +// @public @deprecated export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, ): { headers: Record; }; +// @public +export function getBitbucketServerDefaultBranch( + url: string, + config: BitbucketServerIntegrationConfig, +): Promise; + +// @public +export function getBitbucketServerDownloadUrl( + url: string, + config: BitbucketServerIntegrationConfig, +): Promise; + +// @public +export function getBitbucketServerFileFetchUrl( + url: string, + config: BitbucketServerIntegrationConfig, +): string; + +// @public +export function getBitbucketServerRequestOptions( + config: BitbucketServerIntegrationConfig, +): { + headers: Record; +}; + +// @public +export function getGerritBranchApiUrl( + config: GerritIntegrationConfig, + url: string, +): string; + +// @public +export function getGerritCloneRepoUrl( + config: GerritIntegrationConfig, + url: string, +): string; + // @public export function getGerritFileContentsApiUrl( config: GerritIntegrationConfig, url: string, ): string; +// @public +export function getGerritProjectsApiUrl( + config: GerritIntegrationConfig, +): string; + // @public export function getGerritRequestOptions(config: GerritIntegrationConfig): { headers?: Record; @@ -332,9 +457,13 @@ export interface IntegrationsByType { awsS3: ScmIntegrationsGroup; // (undocumented) azure: ScmIntegrationsGroup; - // (undocumented) + // @deprecated (undocumented) bitbucket: ScmIntegrationsGroup; // (undocumented) + bitbucketCloud: ScmIntegrationsGroup; + // (undocumented) + bitbucketServer: ScmIntegrationsGroup; + // (undocumented) gerrit: ScmIntegrationsGroup; // (undocumented) github: ScmIntegrationsGroup; @@ -342,6 +471,16 @@ export interface IntegrationsByType { gitlab: ScmIntegrationsGroup; } +// @public +export function parseGerritGitilesUrl( + config: GerritIntegrationConfig, + url: string, +): { + branch: string; + filePath: string; + project: string; +}; + // @public export function parseGerritJsonResponse(response: Response): Promise; @@ -366,15 +505,35 @@ export function readAzureIntegrationConfigs( ): AzureIntegrationConfig[]; // @public +export function readBitbucketCloudIntegrationConfig( + config: Config, +): BitbucketCloudIntegrationConfig; + +// @public +export function readBitbucketCloudIntegrationConfigs( + configs: Config[], +): BitbucketCloudIntegrationConfig[]; + +// @public @deprecated export function readBitbucketIntegrationConfig( config: Config, ): BitbucketIntegrationConfig; -// @public +// @public @deprecated export function readBitbucketIntegrationConfigs( configs: Config[], ): BitbucketIntegrationConfig[]; +// @public +export function readBitbucketServerIntegrationConfig( + config: Config, +): BitbucketServerIntegrationConfig; + +// @public +export function readBitbucketServerIntegrationConfigs( + configs: Config[], +): BitbucketServerIntegrationConfig[]; + // @public export function readGerritIntegrationConfig( config: Config, @@ -416,6 +575,12 @@ export function replaceGitHubUrlType( type: 'blob' | 'tree' | 'edit', ): string; +// @public +export function replaceGitLabUrlType( + url: string, + type: 'blob' | 'tree' | 'edit', +): string; + // @public export interface ScmIntegration { resolveEditUrl(url: string): string; @@ -435,9 +600,13 @@ export interface ScmIntegrationRegistry awsS3: ScmIntegrationsGroup; // (undocumented) azure: ScmIntegrationsGroup; - // (undocumented) + // @deprecated (undocumented) bitbucket: ScmIntegrationsGroup; // (undocumented) + bitbucketCloud: ScmIntegrationsGroup; + // (undocumented) + bitbucketServer: ScmIntegrationsGroup; + // (undocumented) gerrit: ScmIntegrationsGroup; // (undocumented) github: ScmIntegrationsGroup; @@ -458,9 +627,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry { get awsS3(): ScmIntegrationsGroup; // (undocumented) get azure(): ScmIntegrationsGroup; - // (undocumented) + // @deprecated (undocumented) get bitbucket(): ScmIntegrationsGroup; // (undocumented) + get bitbucketCloud(): ScmIntegrationsGroup; + // (undocumented) + get bitbucketServer(): ScmIntegrationsGroup; + // (undocumented) byHost(host: string): ScmIntegration | undefined; // (undocumented) byUrl(url: string | URL): ScmIntegration | undefined; diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index e2d26be28e..c14f5c76cd 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -31,7 +31,10 @@ export interface Config { token?: string; }>; - /** Integration configuration for Bitbucket */ + /** + * Integration configuration for Bitbucket + * @deprecated replaced by bitbucketCloud and bitbucketServer + */ bitbucket?: Array<{ /** * The hostname of the given Bitbucket instance @@ -60,6 +63,39 @@ export interface Config { appPassword?: string; }>; + /** Integration configuration for Bitbucket Cloud */ + bitbucketCloud?: Array<{ + /** + * The username to use for authenticated requests. + * @visibility secret + */ + username: string; + /** + * Bitbucket Cloud app password used to authenticate requests. + * @visibility secret + */ + appPassword: string; + }>; + + /** Integration configuration for Bitbucket Server */ + bitbucketServer?: Array<{ + /** + * The hostname of the given Bitbucket Server instance + * @visibility frontend + */ + host: string; + /** + * Token used to authenticate requests. + * @visibility secret + */ + token?: string; + /** + * The base url for the Bitbucket Server API, for example https:///rest/api/1.0 + * @visibility frontend + */ + apiBaseUrl?: string; + }>; + /** Integration configuration for Gerrit */ gerrit?: Array<{ /** @@ -72,6 +108,11 @@ export interface Config { * @visibility frontend */ baseUrl?: string; + /** + * The base url for cloning repos. + * @visibility frontend + */ + cloneUrl?: string; /** * The username to use for authenticated requests. * @visibility secret diff --git a/packages/integration/package.json b/packages/integration/package.json index f0f2c449c1..0e0e2bf18e 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "1.1.0-next.1", + "version": "1.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,9 +42,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/config-loader": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/config-loader": "^1.1.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index 2e6f2f1472..d9fcaf36f0 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -17,8 +17,16 @@ import { AwsS3IntegrationConfig } from './awsS3'; import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AzureIntegrationConfig } from './azure'; import { AzureIntegration } from './azure/AzureIntegration'; +import { + BitbucketCloudIntegration, + BitbucketCloudIntegrationConfig, +} from './bitbucketCloud'; import { BitbucketIntegrationConfig } from './bitbucket'; import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { + BitbucketServerIntegration, + BitbucketServerIntegrationConfig, +} from './bitbucketServer'; import { GerritIntegrationConfig } from './gerrit'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GitHubIntegrationConfig } from './github'; @@ -41,6 +49,14 @@ describe('ScmIntegrations', () => { host: 'bitbucket.local', } as BitbucketIntegrationConfig); + const bitbucketCloud = new BitbucketCloudIntegration({ + host: 'bitbucket.org', + } as BitbucketCloudIntegrationConfig); + + const bitbucketServer = new BitbucketServerIntegration({ + host: 'bitbucket-server.local', + } as BitbucketServerIntegrationConfig); + const gerrit = new GerritIntegration({ host: 'gerrit.local', } as GerritIntegrationConfig); @@ -57,6 +73,11 @@ describe('ScmIntegrations', () => { awsS3: basicIntegrations([awsS3], item => item.config.host), azure: basicIntegrations([azure], item => item.config.host), bitbucket: basicIntegrations([bitbucket], item => item.config.host), + bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title), + bitbucketServer: basicIntegrations( + [bitbucketServer], + item => item.config.host, + ), gerrit: basicIntegrations([gerrit], item => item.config.host), github: basicIntegrations([github], item => item.config.host), gitlab: basicIntegrations([gitlab], item => item.config.host), @@ -66,6 +87,12 @@ describe('ScmIntegrations', () => { expect(i.awsS3.byUrl('https://awss3.local')).toBe(awsS3); expect(i.azure.byUrl('https://azure.local')).toBe(azure); expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe( + bitbucketCloud, + ); + expect(i.bitbucketServer.byUrl('https://bitbucket-server.local')).toBe( + bitbucketServer, + ); expect(i.gerrit.byUrl('https://gerrit.local')).toBe(gerrit); expect(i.github.byUrl('https://github.local')).toBe(github); expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab); @@ -73,7 +100,16 @@ describe('ScmIntegrations', () => { it('can list', () => { expect(i.list()).toEqual( - expect.arrayContaining([awsS3, azure, bitbucket, gerrit, github, gitlab]), + expect.arrayContaining([ + awsS3, + azure, + bitbucket, + bitbucketCloud, + bitbucketServer, + gerrit, + github, + gitlab, + ]), ); }); @@ -81,6 +117,8 @@ describe('ScmIntegrations', () => { expect(i.byUrl('https://awss3.local')).toBe(awsS3); expect(i.byUrl('https://azure.local')).toBe(azure); expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud); + expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer); expect(i.byUrl('https://gerrit.local')).toBe(gerrit); expect(i.byUrl('https://github.local')).toBe(github); expect(i.byUrl('https://gitlab.local')).toBe(gitlab); @@ -88,6 +126,8 @@ describe('ScmIntegrations', () => { expect(i.byHost('awss3.local')).toBe(awsS3); expect(i.byHost('azure.local')).toBe(azure); expect(i.byHost('bitbucket.local')).toBe(bitbucket); + expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud); + expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer); expect(i.byHost('gerrit.local')).toBe(gerrit); expect(i.byHost('github.local')).toBe(github); expect(i.byHost('gitlab.local')).toBe(gitlab); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 8638b814ec..3e712cc012 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -17,7 +17,9 @@ import { Config } from '@backstage/config'; import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; @@ -33,7 +35,12 @@ import { ScmIntegrationRegistry } from './registry'; export interface IntegrationsByType { awsS3: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; + /** + * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` + */ bitbucket: ScmIntegrationsGroup; + bitbucketCloud: ScmIntegrationsGroup; + bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; @@ -52,6 +59,8 @@ export class ScmIntegrations implements ScmIntegrationRegistry { awsS3: AwsS3Integration.factory({ config }), azure: AzureIntegration.factory({ config }), bitbucket: BitbucketIntegration.factory({ config }), + bitbucketCloud: BitbucketCloudIntegration.factory({ config }), + bitbucketServer: BitbucketServerIntegration.factory({ config }), gerrit: GerritIntegration.factory({ config }), github: GitHubIntegration.factory({ config }), gitlab: GitLabIntegration.factory({ config }), @@ -70,10 +79,21 @@ export class ScmIntegrations implements ScmIntegrationRegistry { return this.byType.azure; } + /** + * @deprecated in favor of `bitbucketCloud()` and `bitbucketServer()` + */ get bitbucket(): ScmIntegrationsGroup { return this.byType.bitbucket; } + get bitbucketCloud(): ScmIntegrationsGroup { + return this.byType.bitbucketCloud; + } + + get bitbucketServer(): ScmIntegrationsGroup { + return this.byType.bitbucketServer; + } + get gerrit(): ScmIntegrationsGroup { return this.byType.gerrit; } diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 6bbe1be27b..575e1c9b6f 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -18,25 +18,52 @@ import { ConfigReader } from '@backstage/config'; import { BitbucketIntegration } from './BitbucketIntegration'; describe('BitbucketIntegration', () => { - it('has a working factory', () => { - const integrations = BitbucketIntegration.factory({ - config: new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'h.com', - apiBaseUrl: 'a', - token: 't', - username: 'u', - appPassword: 'p', - }, - ], - }, - }), + describe('factory', () => { + it('works', () => { + const integrations = BitbucketIntegration.factory({ + config: new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'h.com', + apiBaseUrl: 'a', + token: 't', + username: 'u', + appPassword: 'p', + }, + ], + }, + }), + }); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('bitbucket.org'); + }); + + it('falls back to bitbucketCloud+bitbucketServer', () => { + const integrations = BitbucketIntegration.factory({ + config: new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + bitbucketServer: [ + { + host: 'h.com', + apiBaseUrl: 'a', + token: 't', + }, + ], + }, + }), + }); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('bitbucket.org'); + expect(integrations.list()[1].config.host).toBe('h.com'); }); - expect(integrations.list().length).toBe(2); // including default - expect(integrations.list()[0].config.host).toBe('h.com'); - expect(integrations.list()[1].config.host).toBe('bitbucket.org'); }); it('returns the basics', () => { diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index 0162463e3c..d92532b54a 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -26,13 +26,21 @@ import { * A Bitbucket based integration. * * @public + * @deprecated replaced by the integrations bitbucketCloud and bitbucketServer. */ export class BitbucketIntegration implements ScmIntegration { static factory: ScmIntegrationsFactory = ({ config, }) => { const configs = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], + config.getOptionalConfigArray('integrations.bitbucket') ?? [ + // if integrations.bitbucket was not used assume the use was migrated to the new configs + // and backport for the deprecated integration to be usable for other parts of the system + // until these got migrated + ...(config.getOptionalConfigArray('integrations.bitbucketCloud') ?? []), + ...(config.getOptionalConfigArray('integrations.bitbucketServer') ?? + []), + ], ); return basicIntegrations( configs.map(c => new BitbucketIntegration(c)), diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index 44a2f0cc1f..11839094da 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -25,6 +25,7 @@ const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0'; * The configuration parameters for a single Bitbucket API provider. * * @public + * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export type BitbucketIntegrationConfig = { /** @@ -68,6 +69,7 @@ export type BitbucketIntegrationConfig = { * * @param config - The config object of a single integration * @public + * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export function readBitbucketIntegrationConfig( config: Config, @@ -107,6 +109,7 @@ export function readBitbucketIntegrationConfig( * * @param configs - All of the integration config objects * @public + * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export function readBitbucketIntegrationConfigs( configs: Config[], diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index da10152bf8..f6018ad27a 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -24,6 +24,7 @@ import { BitbucketIntegrationConfig } from './config'; * @param url - A URL pointing to a path * @param config - The relevant provider config * @public + * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export async function getBitbucketDefaultBranch( url: string, @@ -75,6 +76,7 @@ export async function getBitbucketDefaultBranch( * @param url - A URL pointing to a path * @param config - The relevant provider config * @public + * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export async function getBitbucketDownloadUrl( url: string, @@ -119,6 +121,7 @@ export async function getBitbucketDownloadUrl( * @param url - A URL pointing to a file * @param config - The relevant provider config * @public + * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export function getBitbucketFileFetchUrl( url: string, @@ -155,6 +158,7 @@ export function getBitbucketFileFetchUrl( * * @param config - The relevant provider config * @public + * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. */ export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, diff --git a/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.test.ts b/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.test.ts new file mode 100644 index 0000000000..4516b78f90 --- /dev/null +++ b/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { BitbucketCloudIntegration } from './BitbucketCloudIntegration'; + +describe('BitbucketCloudIntegration', () => { + it('has a working factory', () => { + const integrations = BitbucketCloudIntegration.factory({ + config: new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + }), + }); + expect(integrations.list().length).toBe(1); + expect(integrations.list()[0].config.username).toBe('u'); + expect(integrations.list()[0].config.appPassword).toBe('p'); + }); + + it('returns the basics', () => { + const integration = new BitbucketCloudIntegration({ + host: 'bitbucket.org', + } as any); + expect(integration.type).toBe('bitbucketCloud'); + expect(integration.title).toBe('bitbucket.org'); + }); + + it('resolves url line number correctly', () => { + const integration = new BitbucketCloudIntegration({} as any); + + expect( + integration.resolveUrl({ + url: './a.yaml', + base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', + lineNumber: 14, + }), + ).toBe( + 'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14', + ); + }); + + it('resolve edit URL', () => { + const integration = new BitbucketCloudIntegration({} as any); + + expect( + integration.resolveEditUrl( + 'https://bitbucket.org/my-owner/my-project/src/master/README.md', + ), + ).toBe( + 'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&at=master', + ); + }); +}); diff --git a/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.ts b/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.ts new file mode 100644 index 0000000000..d05c1fd4a0 --- /dev/null +++ b/packages/integration/src/bitbucketCloud/BitbucketCloudIntegration.ts @@ -0,0 +1,85 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; +import { + BitbucketCloudIntegrationConfig, + readBitbucketCloudIntegrationConfigs, +} from './config'; + +/** + * A Bitbucket Cloud based integration. + * + * @public + */ +export class BitbucketCloudIntegration implements ScmIntegration { + static factory: ScmIntegrationsFactory = ({ + config, + }) => { + const configs = readBitbucketCloudIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucketCloud') ?? [], + ); + return basicIntegrations( + configs.map(c => new BitbucketCloudIntegration(c)), + i => i.config.host, + ); + }; + + constructor( + private readonly integrationConfig: BitbucketCloudIntegrationConfig, + ) {} + + get type(): string { + return 'bitbucketCloud'; + } + + get title(): string { + return this.integrationConfig.host; + } + + get config(): BitbucketCloudIntegrationConfig { + return this.integrationConfig; + } + + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number; + }): string { + const resolved = defaultScmResolveUrl(options); + + // Bitbucket Cloud line numbers use the syntax #lines-42, rather than #L42 + if (options.lineNumber) { + const url = new URL(resolved); + + url.hash = `lines-${options.lineNumber}`; + return url.toString(); + } + + return resolved; + } + + resolveEditUrl(url: string): string { + const urlData = parseGitUrl(url); + const editUrl = new URL(url); + + editUrl.searchParams.set('mode', 'edit'); + editUrl.searchParams.set('at', urlData.ref); + return editUrl.toString(); + } +} diff --git a/packages/integration/src/bitbucketCloud/config.test.ts b/packages/integration/src/bitbucketCloud/config.test.ts new file mode 100644 index 0000000000..9f7a66e2bf --- /dev/null +++ b/packages/integration/src/bitbucketCloud/config.test.ts @@ -0,0 +1,138 @@ +/* + * 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 { Config, ConfigReader } from '@backstage/config'; +import { loadConfigSchema } from '@backstage/config-loader'; +import { + BitbucketCloudIntegrationConfig, + readBitbucketCloudIntegrationConfig, + readBitbucketCloudIntegrationConfigs, +} from './config'; + +describe('readBitbucketCloudIntegrationConfig', () => { + function buildConfig(data: Partial): Config { + return new ConfigReader(data); + } + + async function buildFrontendConfig( + data: Partial, + ): Promise { + const fullSchema = await loadConfigSchema({ + dependencies: ['@backstage/integration'], + }); + const serializedSchema = fullSchema.serialize() as { + schemas: { value: { properties?: { integrations?: object } } }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // only include schemas that apply to integrations + schemas: serializedSchema.schemas.filter( + s => s.value?.properties?.integrations, + ), + }, + }); + const processed = schema.process( + [{ data: { integrations: { bitbucketCloud: [data] } }, context: 'app' }], + { visibility: ['frontend'] }, + ); + return new ConfigReader(processed[0].data as any); + } + + it('reads all values', () => { + const output = readBitbucketCloudIntegrationConfig( + buildConfig({ + username: 'u', + appPassword: 'p', + }), + ); + expect(output).toEqual({ + apiBaseUrl: 'https://api.bitbucket.org/2.0', + appPassword: 'p', + host: 'bitbucket.org', + username: 'u', + }); + }); + + it('rejects funky configs', () => { + const valid: any = { + username: 'u', + appPassword: 'p', + }; + expect(() => + readBitbucketCloudIntegrationConfig( + buildConfig({ ...valid, username: 7 }), + ), + ).toThrow(/username/); + expect(() => + readBitbucketCloudIntegrationConfig( + buildConfig({ ...valid, appPassword: 7 }), + ), + ).toThrow(/appPassword/); + }); + + it('credentials hidden on the frontend', async () => { + const frontendConfig = await buildFrontendConfig({ + appPassword: 'p', + username: 'u', + }); + expect( + readBitbucketCloudIntegrationConfigs( + frontendConfig.getOptionalConfigArray('integrations.bitbucketCloud') ?? + [], + ), + ).toEqual([ + { + apiBaseUrl: 'https://api.bitbucket.org/2.0', + host: 'bitbucket.org', + }, + ]); + }); +}); + +describe('readBitbucketCloudIntegrationConfigs', () => { + function buildConfig( + data: Partial[], + ): Config[] { + return data.map(item => new ConfigReader(item)); + } + + it('reads all values', () => { + const output = readBitbucketCloudIntegrationConfigs( + buildConfig([ + { + username: 'u', + appPassword: 'p', + }, + ]), + ); + expect(output).toContainEqual({ + apiBaseUrl: 'https://api.bitbucket.org/2.0', + appPassword: 'p', + host: 'bitbucket.org', + username: 'u', + }); + }); + + it('adds a default Bitbucket Cloud entry when missing', () => { + const output = readBitbucketCloudIntegrationConfigs(buildConfig([])); + expect(output).toEqual([ + { + apiBaseUrl: 'https://api.bitbucket.org/2.0', + host: 'bitbucket.org', + }, + ]); + }); +}); diff --git a/packages/integration/src/bitbucketCloud/config.ts b/packages/integration/src/bitbucketCloud/config.ts new file mode 100644 index 0000000000..710957900e --- /dev/null +++ b/packages/integration/src/bitbucketCloud/config.ts @@ -0,0 +1,98 @@ +/* + * 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 { Config } from '@backstage/config'; + +const BITBUCKET_CLOUD_HOST = 'bitbucket.org'; +const BITBUCKET_CLOUD_API_BASE_URL = 'https://api.bitbucket.org/2.0'; + +/** + * The configuration parameters for a single Bitbucket Cloud API provider. + * + * @public + */ +export type BitbucketCloudIntegrationConfig = { + /** + * Constant. bitbucket.org + */ + host: string; + + /** + * Constant. https://api.bitbucket.org/2.0 + */ + apiBaseUrl: string; + + /** + * The username to use for requests to Bitbucket Cloud (bitbucket.org). + */ + username?: string; + + /** + * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords. + * + * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ + */ + appPassword?: string; +}; + +/** + * Reads a single Bitbucket Cloud integration config. + * + * @param config - The config object of a single integration + * @public + */ +export function readBitbucketCloudIntegrationConfig( + config: Config, +): BitbucketCloudIntegrationConfig { + const host = BITBUCKET_CLOUD_HOST; + const apiBaseUrl = BITBUCKET_CLOUD_API_BASE_URL; + // If config is provided, we assume authenticated access is desired + // (as the anonymous one is provided by default). + const username = config.getString('username'); + const appPassword = config.getString('appPassword'); + + return { + host, + apiBaseUrl, + username, + appPassword, + }; +} + +/** + * Reads a set of Bitbucket Cloud integration configs, + * and inserts one for public Bitbucket Cloud if none specified. + * + * @param configs - All of the integration config objects + * @public + */ +export function readBitbucketCloudIntegrationConfigs( + configs: Config[], +): BitbucketCloudIntegrationConfig[] { + // First read all the explicit integrations + const result = configs.map(readBitbucketCloudIntegrationConfig); + + // If no explicit bitbucket.org integration was added, + // put one in the list as a convenience + if (result.length === 0) { + result.push({ + host: BITBUCKET_CLOUD_HOST, + apiBaseUrl: BITBUCKET_CLOUD_API_BASE_URL, + }); + } + + return result; +} diff --git a/packages/integration/src/bitbucketCloud/core.test.ts b/packages/integration/src/bitbucketCloud/core.test.ts new file mode 100644 index 0000000000..5ce2b74862 --- /dev/null +++ b/packages/integration/src/bitbucketCloud/core.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { BitbucketCloudIntegrationConfig } from './config'; +import { + getBitbucketCloudDefaultBranch, + getBitbucketCloudDownloadUrl, + getBitbucketCloudFileFetchUrl, + getBitbucketCloudRequestOptions, +} from './core'; + +describe('bitbucketCloud core', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + describe('getBitbucketCloudRequestOptions', () => { + it('insert basic auth when needed', () => { + const withUsernameAndPassword: BitbucketCloudIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + username: 'some-user', + appPassword: 'my-secret', + }; + const withoutUsernameAndPassword: BitbucketCloudIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }; + expect( + ( + getBitbucketCloudRequestOptions(withUsernameAndPassword) + .headers as any + ).Authorization, + ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); + expect( + ( + getBitbucketCloudRequestOptions(withoutUsernameAndPassword) + .headers as any + ).Authorization, + ).toBeUndefined(); + }); + }); + + describe('getBitbucketCloudFileFetchUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: BitbucketCloudIntegrationConfig = { + host: '', + apiBaseUrl: '', + }; + expect(() => getBitbucketCloudFileFetchUrl('a/b', config)).toThrow( + /Incorrect URL: a\/b/, + ); + }); + + it('happy path for Bitbucket Cloud', () => { + const config: BitbucketCloudIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }; + expect( + getBitbucketCloudFileFetchUrl( + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config, + ), + ).toEqual( + 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', + ); + }); + }); + + describe('getBitbucketCloudDownloadUrl', () => { + it('do not add path param for Bitbucket Cloud', async () => { + const config: BitbucketCloudIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }; + const result = await getBitbucketCloudDownloadUrl( + 'https://bitbucket.org/backstage/mock/src/master', + config, + ); + expect(result).toEqual( + 'https://bitbucket.org/backstage/mock/get/master.tar.gz', + ); + }); + }); + + describe('getBitbucketCloudDefaultBranch', () => { + it('return default branch for Bitbucket Cloud', async () => { + const repoInfoResponse = { + mainbranch: { + name: 'main', + }, + }; + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoInfoResponse), + ), + ), + ); + const config: BitbucketCloudIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }; + const defaultBranch = await getBitbucketCloudDefaultBranch( + 'https://bitbucket.org/backstage/mock/src/main', + config, + ); + expect(defaultBranch).toEqual('main'); + }); + }); +}); diff --git a/packages/integration/src/bitbucketCloud/core.ts b/packages/integration/src/bitbucketCloud/core.ts new file mode 100644 index 0000000000..37feb5dad6 --- /dev/null +++ b/packages/integration/src/bitbucketCloud/core.ts @@ -0,0 +1,140 @@ +/* + * 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 fetch from 'cross-fetch'; +import parseGitUrl from 'git-url-parse'; +import { BitbucketCloudIntegrationConfig } from './config'; + +/** + * Given a URL pointing to a path on a provider, returns the default branch. + * + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public + */ +export async function getBitbucketCloudDefaultBranch( + url: string, + config: BitbucketCloudIntegrationConfig, +): Promise { + const { name: repoName, owner: project } = parseGitUrl(url); + + const branchUrl = `${config.apiBaseUrl}/repositories/${project}/${repoName}`; + const response = await fetch( + branchUrl, + getBitbucketCloudRequestOptions(config), + ); + + if (!response.ok) { + const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; + throw new Error(message); + } + + const repoInfo = await response.json(); + const defaultBranch = repoInfo.mainbranch.name; + if (!defaultBranch) { + throw new Error( + `Failed to read default branch from ${branchUrl}. ` + + `Response ${response.status} ${response.json()}`, + ); + } + return defaultBranch; +} + +/** + * Given a URL pointing to a path on a provider, returns a URL that is suitable + * for downloading the subtree. + * + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public + */ +export async function getBitbucketCloudDownloadUrl( + url: string, + config: BitbucketCloudIntegrationConfig, +): Promise { + const { + name: repoName, + owner: project, + ref, + protocol, + resource, + } = parseGitUrl(url); + + let branch = ref; + if (!branch) { + branch = await getBitbucketCloudDefaultBranch(url, config); + } + return `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz`; +} + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * @remarks + * + * Converts + * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml + * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml + * + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public + */ +export function getBitbucketCloudFileFetchUrl( + url: string, + config: BitbucketCloudIntegrationConfig, +): string { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) { + throw new Error('Invalid Bitbucket Cloud URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + + if (!ref) { + throw new Error('Invalid Bitbucket Cloud URL or file path'); + } + return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`; + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config - The relevant provider config + * @public + */ +export function getBitbucketCloudRequestOptions( + config: BitbucketCloudIntegrationConfig, +): { headers: Record } { + const headers: Record = {}; + + if (config.username && config.appPassword) { + const buffer = Buffer.from( + `${config.username}:${config.appPassword}`, + 'utf8', + ); + headers.Authorization = `Basic ${buffer.toString('base64')}`; + } + + return { + headers, + }; +} diff --git a/packages/integration/src/bitbucketCloud/index.ts b/packages/integration/src/bitbucketCloud/index.ts new file mode 100644 index 0000000000..7119d3ea90 --- /dev/null +++ b/packages/integration/src/bitbucketCloud/index.ts @@ -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. + */ + +export { BitbucketCloudIntegration } from './BitbucketCloudIntegration'; +export { + readBitbucketCloudIntegrationConfig, + readBitbucketCloudIntegrationConfigs, +} from './config'; +export type { BitbucketCloudIntegrationConfig } from './config'; +export { + getBitbucketCloudDefaultBranch, + getBitbucketCloudDownloadUrl, + getBitbucketCloudFileFetchUrl, + getBitbucketCloudRequestOptions, +} from './core'; diff --git a/packages/integration/src/bitbucketServer/BitbucketServerIntegration.test.ts b/packages/integration/src/bitbucketServer/BitbucketServerIntegration.test.ts new file mode 100644 index 0000000000..24d7935e72 --- /dev/null +++ b/packages/integration/src/bitbucketServer/BitbucketServerIntegration.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { BitbucketServerIntegration } from './BitbucketServerIntegration'; + +describe('BitbucketServerIntegration', () => { + it('has a working factory', () => { + const integrations = BitbucketServerIntegration.factory({ + config: new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: 'h.com', + apiBaseUrl: 'a', + token: 't', + }, + ], + }, + }), + }); + expect(integrations.list().length).toBe(1); + expect(integrations.list()[0].config.host).toBe('h.com'); + }); + + it('returns the basics', () => { + const integration = new BitbucketServerIntegration({ + host: 'h.com', + } as any); + expect(integration.type).toBe('bitbucketServer'); + expect(integration.title).toBe('h.com'); + }); + + it('resolves url line number correctly', () => { + const integration = new BitbucketServerIntegration({ + host: 'h.com', + } as any); + + expect( + integration.resolveUrl({ + url: './a.yaml', + base: 'https://h.com/my-owner/my-project/src/master/README.md', + lineNumber: 14, + }), + ).toBe('https://h.com/my-owner/my-project/src/master/a.yaml#a.yaml-14'); + }); + + it('resolve edit URL', () => { + const integration = new BitbucketServerIntegration({ + host: 'h.com', + } as any); + + expect( + integration.resolveEditUrl( + 'https://h.com/my-owner/my-project/src/master/README.md', + ), + ).toBe( + 'https://h.com/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master', + ); + }); +}); diff --git a/packages/integration/src/bitbucketServer/BitbucketServerIntegration.ts b/packages/integration/src/bitbucketServer/BitbucketServerIntegration.ts new file mode 100644 index 0000000000..10055aeddc --- /dev/null +++ b/packages/integration/src/bitbucketServer/BitbucketServerIntegration.ts @@ -0,0 +1,89 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; +import { + BitbucketServerIntegrationConfig, + readBitbucketServerIntegrationConfigs, +} from './config'; + +/** + * A Bitbucket Server based integration. + * + * @public + */ +export class BitbucketServerIntegration implements ScmIntegration { + static factory: ScmIntegrationsFactory = ({ + config, + }) => { + const configs = readBitbucketServerIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucketServer') ?? [], + ); + return basicIntegrations( + configs.map(c => new BitbucketServerIntegration(c)), + i => i.config.host, + ); + }; + + constructor( + private readonly integrationConfig: BitbucketServerIntegrationConfig, + ) {} + + get type(): string { + return 'bitbucketServer'; + } + + get title(): string { + return this.integrationConfig.host; + } + + get config(): BitbucketServerIntegrationConfig { + return this.integrationConfig; + } + + resolveUrl(options: { + url: string; + base: string; + lineNumber?: number; + }): string { + const resolved = defaultScmResolveUrl(options); + + // Bitbucket Server line numbers use the syntax #example.txt-42, rather than #L42 + if (options.lineNumber) { + const url = new URL(resolved); + + const filename = url.pathname.split('/').slice(-1)[0]; + url.hash = `${filename}-${options.lineNumber}`; + return url.toString(); + } + + return resolved; + } + + resolveEditUrl(url: string): string { + const urlData = parseGitUrl(url); + const editUrl = new URL(url); + + editUrl.searchParams.set('mode', 'edit'); + // TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it + // but this is taken over from the initial implementation. + editUrl.searchParams.set('spa', '0'); + editUrl.searchParams.set('at', urlData.ref); + return editUrl.toString(); + } +} diff --git a/packages/integration/src/bitbucketServer/config.test.ts b/packages/integration/src/bitbucketServer/config.test.ts new file mode 100644 index 0000000000..83c28f5e9d --- /dev/null +++ b/packages/integration/src/bitbucketServer/config.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { Config, ConfigReader } from '@backstage/config'; +import { loadConfigSchema } from '@backstage/config-loader'; +import { + BitbucketServerIntegrationConfig, + readBitbucketServerIntegrationConfig, + readBitbucketServerIntegrationConfigs, +} from './config'; + +describe('readBitbucketServerIntegrationConfig', () => { + function buildConfig( + data: Partial, + ): Config { + return new ConfigReader(data); + } + + async function buildFrontendConfig( + data: Partial, + ): Promise { + const fullSchema = await loadConfigSchema({ + dependencies: ['@backstage/integration'], + }); + const serializedSchema = fullSchema.serialize() as { + schemas: { value: { properties?: { integrations?: object } } }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // only include schemas that apply to integrations + schemas: serializedSchema.schemas.filter( + s => s.value?.properties?.integrations, + ), + }, + }); + const processed = schema.process( + [{ data: { integrations: { bitbucketServer: [data] } }, context: 'app' }], + { visibility: ['frontend'] }, + ); + return new ConfigReader( + (processed[0].data as any).integrations.bitbucketServer[0], + ); + } + + it('reads all values', () => { + const output = readBitbucketServerIntegrationConfig( + buildConfig({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }), + ); + expect(output).toEqual({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }); + }); + + it('rejects funky configs', () => { + const valid: any = { + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }; + expect(() => + readBitbucketServerIntegrationConfig(buildConfig({ ...valid, host: 7 })), + ).toThrow(/host/); + expect(() => + readBitbucketServerIntegrationConfig( + buildConfig({ ...valid, apiBaseUrl: 7 }), + ), + ).toThrow(/apiBaseUrl/); + expect(() => + readBitbucketServerIntegrationConfig(buildConfig({ ...valid, token: 7 })), + ).toThrow(/token/); + }); + + it('works on the frontend', async () => { + expect( + readBitbucketServerIntegrationConfig( + await buildFrontendConfig({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }), + ), + ).toEqual({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + }); + }); +}); + +describe('readBitbucketServerIntegrationConfigs', () => { + function buildConfig( + data: Partial[], + ): Config[] { + return data.map(item => new ConfigReader(item)); + } + + it('reads all values', () => { + const output = readBitbucketServerIntegrationConfigs( + buildConfig([ + { + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }, + ]), + ); + expect(output).toContainEqual({ + host: 'a.com', + apiBaseUrl: 'https://a.com/api', + token: 't', + }); + }); + + it('adds no default Bitbucket Server entry when missing', () => { + const output = readBitbucketServerIntegrationConfigs(buildConfig([])); + expect(output).toEqual([]); + }); + + it('injects the correct Bitbucket Server API base URL when missing', () => { + const output = readBitbucketServerIntegrationConfigs( + buildConfig([{ host: 'bitbucket.company.com' }]), + ); + expect(output).toEqual([ + { + host: 'bitbucket.company.com', + apiBaseUrl: 'https://bitbucket.company.com/rest/api/1.0', + }, + ]); + }); +}); diff --git a/packages/integration/src/bitbucketServer/config.ts b/packages/integration/src/bitbucketServer/config.ts new file mode 100644 index 0000000000..ec7930616c --- /dev/null +++ b/packages/integration/src/bitbucketServer/config.ts @@ -0,0 +1,95 @@ +/* + * 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 { Config } from '@backstage/config'; +import { trimEnd } from 'lodash'; +import { isValidHost } from '../helpers'; + +/** + * The configuration parameters for a single Bitbucket Server API provider. + * + * @public + */ +export type BitbucketServerIntegrationConfig = { + /** + * The host of the target that this matches on, e.g. "bitbucket.company.com" + */ + host: string; + + /** + * The base URL of the API of this provider, e.g. "https:///rest/api/1.0", + * with no trailing slash. + * + * The API will always be preferred if both its base URL and a token are + * present. + */ + apiBaseUrl: string; + + /** + * The authorization token to use for requests to a Bitbucket Server provider. + * + * See https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html + * + * If no token is specified, anonymous access is used. + */ + token?: string; +}; + +/** + * Reads a single Bitbucket Server integration config. + * + * @param config - The config object of a single integration + * @public + */ +export function readBitbucketServerIntegrationConfig( + config: Config, +): BitbucketServerIntegrationConfig { + const host = config.getString('host'); + let apiBaseUrl = config.getOptionalString('apiBaseUrl'); + const token = config.getOptionalString('token'); + + if (!isValidHost(host)) { + throw new Error( + `Invalid Bitbucket Server integration config, '${host}' is not a valid host`, + ); + } + + if (apiBaseUrl) { + apiBaseUrl = trimEnd(apiBaseUrl, '/'); + } else { + apiBaseUrl = `https://${host}/rest/api/1.0`; + } + + return { + host, + apiBaseUrl, + token, + }; +} + +/** + * Reads a set of Bitbucket Server integration configs. + * + * @param configs - All of the integration config objects + * @public + */ +export function readBitbucketServerIntegrationConfigs( + configs: Config[], +): BitbucketServerIntegrationConfig[] { + // Read all the explicit integrations + // No default integration will be added + return configs.map(readBitbucketServerIntegrationConfig); +} diff --git a/packages/integration/src/bitbucketServer/core.test.ts b/packages/integration/src/bitbucketServer/core.test.ts new file mode 100644 index 0000000000..1c8f5d259c --- /dev/null +++ b/packages/integration/src/bitbucketServer/core.test.ts @@ -0,0 +1,217 @@ +/* + * 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 { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { BitbucketServerIntegrationConfig } from './config'; +import { + getBitbucketServerDefaultBranch, + getBitbucketServerDownloadUrl, + getBitbucketServerFileFetchUrl, + getBitbucketServerRequestOptions, +} from './core'; + +describe('bitbucketServer core', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + describe('getBitbucketServerRequestOptions', () => { + it('inserts a token when needed', () => { + const withToken: BitbucketServerIntegrationConfig = { + host: '', + apiBaseUrl: '', + token: 'A', + }; + const withoutToken: BitbucketServerIntegrationConfig = { + host: '', + apiBaseUrl: '', + }; + expect( + (getBitbucketServerRequestOptions(withToken).headers as any) + .Authorization, + ).toEqual('Bearer A'); + expect( + (getBitbucketServerRequestOptions(withoutToken).headers as any) + .Authorization, + ).toBeUndefined(); + }); + }); + + describe('getBitbucketServerFileFetchUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: BitbucketServerIntegrationConfig = { + host: '', + apiBaseUrl: '', + }; + expect(() => getBitbucketServerFileFetchUrl('a/b', config)).toThrow( + /Incorrect URL: a\/b/, + ); + }); + + it('happy path for Bitbucket Server', () => { + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0', + }; + expect( + getBitbucketServerFileFetchUrl( + 'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=', + ); + }); + }); + + describe('getBitbucketServerDownloadUrl', () => { + it('add path param if a path is specified for Bitbucket Server', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const result = await getBitbucketServerDownloadUrl( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs', + config, + ); + expect(result).toEqual( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock&path=docs', + ); + }); + + it('do not add path param if no path is specified for Bitbucket Server', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const result = await getBitbucketServerDownloadUrl( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse', + config, + ); + + expect(result).toEqual( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock', + ); + }); + + it('get by branch for Bitbucket Server', async () => { + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const result = await getBitbucketServerDownloadUrl( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', + config, + ); + expect(result).toEqual( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=docs', + ); + }); + }); + + describe('getBitbucketServerDefaultBranch', () => { + it('return default branch for Bitbucket Server', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const defaultBranch = await getBitbucketServerDefaultBranch( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', + config, + ); + expect(defaultBranch).toEqual('main'); + }); + + it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(404), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + const config: BitbucketServerIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const defaultBranch = await getBitbucketServerDefaultBranch( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', + config, + ); + expect(defaultBranch).toEqual('main'); + }); + }); +}); diff --git a/packages/integration/src/bitbucketServer/core.ts b/packages/integration/src/bitbucketServer/core.ts new file mode 100644 index 0000000000..80dbc0443f --- /dev/null +++ b/packages/integration/src/bitbucketServer/core.ts @@ -0,0 +1,145 @@ +/* + * 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 fetch from 'cross-fetch'; +import parseGitUrl from 'git-url-parse'; +import { BitbucketServerIntegrationConfig } from './config'; + +/** + * Given a URL pointing to a path on a provider, returns the default branch. + * + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public + */ +export async function getBitbucketServerDefaultBranch( + url: string, + config: BitbucketServerIntegrationConfig, +): Promise { + const { name: repoName, owner: project } = parseGitUrl(url); + + // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 + let branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; + + let response = await fetch( + branchUrl, + getBitbucketServerRequestOptions(config), + ); + + if (response.status === 404) { + // First try the new format, and then if it gets specifically a 404 it should try the old format + // (to support old Atlassian Bitbucket Server v5.11.1 format ) + branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + response = await fetch(branchUrl, getBitbucketServerRequestOptions(config)); + } + + if (!response.ok) { + const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; + throw new Error(message); + } + + const { displayId } = await response.json(); + const defaultBranch = displayId; + if (!defaultBranch) { + throw new Error( + `Failed to read default branch from ${branchUrl}. ` + + `Response ${response.status} ${response.json()}`, + ); + } + return defaultBranch; +} + +/** + * Given a URL pointing to a path on a provider, returns a URL that is suitable + * for downloading the subtree. + * + * @param url - A URL pointing to a path + * @param config - The relevant provider config + * @public + */ +export async function getBitbucketServerDownloadUrl( + url: string, + config: BitbucketServerIntegrationConfig, +): Promise { + const { name: repoName, owner: project, ref, filepath } = parseGitUrl(url); + + let branch = ref; + if (!branch) { + branch = await getBitbucketServerDefaultBranch(url, config); + } + // path will limit the downloaded content + // /docs will only download the docs folder and everything below it + // /docs/index.md will download the docs folder and everything below it + const path = filepath ? `&path=${encodeURIComponent(filepath)}` : ''; + return `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`; +} + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * @remarks + * + * Converts + * from: https://bitbucket.company.com/projectname/reponame/src/main/file.yaml + * to: https://bitbucket.company.com/rest/api/1.0/project/projectname/reponame/raw/file.yaml?at=main + * + * @param url - A URL pointing to a file + * @param config - The relevant provider config + * @public + */ +export function getBitbucketServerFileFetchUrl( + url: string, + config: BitbucketServerIntegrationConfig, +): string { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + if ( + !owner || + !name || + (filepathtype !== 'browse' && + filepathtype !== 'raw' && + filepathtype !== 'src') + ) { + throw new Error('Invalid Bitbucket Server URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`; + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config - The relevant provider config + * @public + */ +export function getBitbucketServerRequestOptions( + config: BitbucketServerIntegrationConfig, +): { headers: Record } { + const headers: Record = {}; + + if (config.token) { + headers.Authorization = `Bearer ${config.token}`; + } + + return { + headers, + }; +} diff --git a/packages/integration/src/bitbucketServer/index.ts b/packages/integration/src/bitbucketServer/index.ts new file mode 100644 index 0000000000..23683ddbff --- /dev/null +++ b/packages/integration/src/bitbucketServer/index.ts @@ -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. + */ + +export { BitbucketServerIntegration } from './BitbucketServerIntegration'; +export { + readBitbucketServerIntegrationConfig, + readBitbucketServerIntegrationConfigs, +} from './config'; +export type { BitbucketServerIntegrationConfig } from './config'; +export { + getBitbucketServerDefaultBranch, + getBitbucketServerDownloadUrl, + getBitbucketServerFileFetchUrl, + getBitbucketServerRequestOptions, +} from './core'; diff --git a/packages/integration/src/gerrit/config.test.ts b/packages/integration/src/gerrit/config.test.ts index 9c0d022548..f9287f6801 100644 --- a/packages/integration/src/gerrit/config.test.ts +++ b/packages/integration/src/gerrit/config.test.ts @@ -56,6 +56,7 @@ describe('readGerritIntegrationConfig', () => { buildConfig({ host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https:a.com/clone', gitilesBaseUrl: 'https://a.com/git', username: 'u', password: 'p', @@ -64,6 +65,7 @@ describe('readGerritIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https:a.com/clone', gitilesBaseUrl: 'https://a.com/git', username: 'u', password: 'p', @@ -79,6 +81,7 @@ describe('readGerritIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', baseUrl: 'https://a.com', + cloneUrl: 'https://a.com', gitilesBaseUrl: 'https://a.com', username: undefined, password: undefined, @@ -110,6 +113,7 @@ describe('readGerritIntegrationConfig', () => { ).toEqual({ host: 'a.com', baseUrl: 'https://a.com/gerrit', + cloneUrl: 'https://a.com/gerrit', gitilesBaseUrl: 'https://a.com', }); }); @@ -139,6 +143,7 @@ describe('readGerritIntegrationConfigs', () => { { host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https://a.com/api', gitilesBaseUrl: 'https://a.com', username: 'u', password: 'p', @@ -146,6 +151,7 @@ describe('readGerritIntegrationConfigs', () => { { host: 'b.com', baseUrl: 'https://b.com/api', + cloneUrl: 'https://b.com/api', gitilesBaseUrl: 'https://b.com', username: undefined, password: undefined, diff --git a/packages/integration/src/gerrit/config.ts b/packages/integration/src/gerrit/config.ts index 7dbb05d15b..8558ff0c1b 100644 --- a/packages/integration/src/gerrit/config.ts +++ b/packages/integration/src/gerrit/config.ts @@ -38,6 +38,12 @@ export type GerritIntegrationConfig = { */ baseUrl?: string; + /** + * The optional base url to use for cloning a repository. If not set the + * baseUrl will be used. + */ + cloneUrl?: string; + /** * Optional base url for Gitiles. This is needed for creating a valid * user-friendly url that can be used for browsing the content of the @@ -69,6 +75,7 @@ export function readGerritIntegrationConfig( ): GerritIntegrationConfig { const host = config.getString('host'); let baseUrl = config.getOptionalString('baseUrl'); + let cloneUrl = config.getOptionalString('cloneUrl'); let gitilesBaseUrl = config.getOptionalString('gitilesBaseUrl'); const username = config.getOptionalString('username'); const password = config.getOptionalString('password'); @@ -81,6 +88,10 @@ export function readGerritIntegrationConfig( throw new Error( `Invalid Gerrit integration config, '${baseUrl}' is not a valid baseUrl`, ); + } else if (cloneUrl && !isValidUrl(cloneUrl)) { + throw new Error( + `Invalid Gerrit integration config, '${cloneUrl}' is not a valid cloneUrl`, + ); } else if (gitilesBaseUrl && !isValidUrl(gitilesBaseUrl)) { throw new Error( `Invalid Gerrit integration config, '${gitilesBaseUrl}' is not a valid gitilesBaseUrl`, @@ -96,10 +107,16 @@ export function readGerritIntegrationConfig( } else { gitilesBaseUrl = `https://${host}`; } + if (cloneUrl) { + cloneUrl = trimEnd(cloneUrl, '/'); + } else { + cloneUrl = baseUrl; + } return { host, baseUrl, + cloneUrl, gitilesBaseUrl, username, password, diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index b966a31b9e..84bc1c0c19 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,9 +20,11 @@ import fetch from 'cross-fetch'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { GerritIntegrationConfig } from './config'; import { + getGerritBranchApiUrl, + getGerritCloneRepoUrl, getGerritRequestOptions, parseGerritJsonResponse, - parseGitilesUrl, + parseGerritGitilesUrl, getGerritFileContentsApiUrl, } from './core'; @@ -55,7 +57,7 @@ describe('gerrit core', () => { host: 'gerrit.com', gitilesBaseUrl: 'https://gerrit.com/gitiles', }; - const { branch, filePath, project } = parseGitilesUrl( + const { branch, filePath, project } = parseGerritGitilesUrl( config, 'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md', ); @@ -63,7 +65,7 @@ describe('gerrit core', () => { expect(branch).toEqual('master'); expect(filePath).toEqual('README.md'); - const { filePath: rootPath } = parseGitilesUrl( + const { filePath: rootPath } = parseGerritGitilesUrl( config, 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', ); @@ -75,13 +77,13 @@ describe('gerrit core', () => { gitilesBaseUrl: 'https://gerrit.com', }; expect(() => - parseGitilesUrl( + parseGerritGitilesUrl( config, 'https://gerrit.com/+/refs/heads/master/README.md', ), ).toThrow(/project/); expect(() => - parseGitilesUrl( + parseGerritGitilesUrl( config, 'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md', ), @@ -89,6 +91,71 @@ describe('gerrit core', () => { }); }); + describe('getGerritBranchApiUrl', () => { + it('can create an url for anonymous access.', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + const fileContentUrl = getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(fileContentUrl).toEqual( + 'https://gerrit.com/projects/web%2Fproject/branches/master', + ); + }); + it('can create an url for authenticated access.', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + const authFileContentUrl = getGerritBranchApiUrl( + authConfig, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(authFileContentUrl).toEqual( + 'https://gerrit.com/a/projects/web%2Fproject/branches/master', + ); + }); + }); + + describe('getGerritCloneRepoUrl', () => { + it('can create an url for anonymous clone.', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + cloneUrl: 'https://gerrit.com/clone', + gitilesBaseUrl: 'https://gerrit.com', + }; + const fileContentUrl = getGerritCloneRepoUrl( + config, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(fileContentUrl).toEqual('https://gerrit.com/clone/web/project'); + }); + it('can create an url for authenticated clone.', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + cloneUrl: 'https://gerrit.com/clone', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + const authFileContentUrl = getGerritCloneRepoUrl( + authConfig, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(authFileContentUrl).toEqual( + 'https://gerrit.com/clone/a/web/project', + ); + }); + }); + describe('getGerritFileContentsApiUrl', () => { it('can create an url for anonymous access to the file fetch api.', () => { const config: GerritIntegrationConfig = { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 517d27d3ec..eb413a2608 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -18,12 +18,6 @@ import { GerritIntegrationConfig } from '.'; const GERRIT_BODY_PREFIX = ")]}'"; -type GitFile = { - branch: string; - filePath: string; - project: string; -}; - /** * Parse a Gitiles URL and return branch, file path and project. * @@ -41,17 +35,17 @@ type GitFile = { * the urls point to an actual Gitiles installation. * * Gitiles url: - * https://g.com/optional_path/{project}/+/refs/heads/{branch}/{filePath} + * https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} * * * @param url - An URL pointing to a file stored in git. * @public */ -export function parseGitilesUrl( +export function parseGerritGitilesUrl( config: GerritIntegrationConfig, url: string, -): GitFile { +): { branch: string; filePath: string; project: string } { const urlPath = url.replace(config.gitilesBaseUrl!, ''); const parts = urlPath.split('/').filter(p => !!p); @@ -94,9 +88,43 @@ export function getAuthenticationPrefix( return config.password ? '/a/' : '/'; } +/** + * Return the url to get branch info from the Gerrit API. + * + * @param config - A Gerrit provider config. + * @param url - An url pointing to a file in git. + * @public + */ +export function getGerritBranchApiUrl( + config: GerritIntegrationConfig, + url: string, +) { + const { branch, project } = parseGerritGitilesUrl(config, url); + + return `${config.baseUrl}${getAuthenticationPrefix( + config, + )}projects/${encodeURIComponent(project)}/branches/${branch}`; +} + +/** + * Return the url to clone the repo that is referenced by the url. + * + * @param url - An url pointing to a file in git. + * @public + */ +export function getGerritCloneRepoUrl( + config: GerritIntegrationConfig, + url: string, +) { + const { project } = parseGerritGitilesUrl(config, url); + + return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`; +} + /** * Return the url to fetch the contents of a file using the Gerrit API. * + * @param config - A Gerrit provider config. * @param url - An url pointing to a file in git. * @public */ @@ -104,7 +132,7 @@ export function getGerritFileContentsApiUrl( config: GerritIntegrationConfig, url: string, ) { - const { branch, filePath, project } = parseGitilesUrl(config, url); + const { branch, filePath, project } = parseGerritGitilesUrl(config, url); return `${config.baseUrl}${getAuthenticationPrefix( config, @@ -113,6 +141,16 @@ export function getGerritFileContentsApiUrl( )}/branches/${branch}/files/${encodeURIComponent(filePath)}/content`; } +/** + * Return the url to query available projects using the Gerrit API. + * + * @param config - A Gerrit provider config. + * @public + */ +export function getGerritProjectsApiUrl(config: GerritIntegrationConfig) { + return `${config.baseUrl}${getAuthenticationPrefix(config)}projects/`; +} + /** * Return request headers for a Gerrit provider. * diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 75e9779dbc..127c949c4e 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -19,9 +19,13 @@ export { readGerritIntegrationConfigs, } from './config'; export { + getGerritBranchApiUrl, + getGerritCloneRepoUrl, getGerritFileContentsApiUrl, + getGerritProjectsApiUrl, getGerritRequestOptions, parseGerritJsonResponse, + parseGerritGitilesUrl, } from './core'; export type { GerritIntegrationConfig } from './config'; diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 01e42e0984..575ce75308 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitLabIntegration, replaceUrlType } from './GitLabIntegration'; +import { GitLabIntegration, replaceGitLabUrlType } from './GitLabIntegration'; describe('GitLabIntegration', () => { it('has a working factory', () => { @@ -55,28 +55,28 @@ describe('GitLabIntegration', () => { }); }); -describe('replaceUrlType', () => { +describe('replaceGitLabUrlType', () => { it('should replace with expected type', () => { expect( - replaceUrlType( + replaceGitLabUrlType( 'https://gitlab.com/my-org/my-project/-/blob/develop/README.md', 'edit', ), ).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md'); expect( - replaceUrlType( + replaceGitLabUrlType( 'https://gitlab.com/webmodules/blob/-/blob/develop/test', 'tree', ), ).toBe('https://gitlab.com/webmodules/blob/-/tree/develop/test'); expect( - replaceUrlType( + replaceGitLabUrlType( 'https://gitlab.com/blob/blob/-/blob/develop/test', 'tree', ), ).toBe('https://gitlab.com/blob/blob/-/tree/develop/test'); expect( - replaceUrlType( + replaceGitLabUrlType( 'https://gitlab.com/blob/blob/-/edit/develop/README.md', 'tree', ), diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index cb24829946..0c52799599 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -60,11 +60,18 @@ export class GitLabIntegration implements ScmIntegration { } resolveEditUrl(url: string): string { - return replaceUrlType(url, 'edit'); + return replaceGitLabUrlType(url, 'edit'); } } -export function replaceUrlType( +/** + * Takes a GitLab URL and replaces the type part (blob, tree etc). + * + * @param url - The original URL + * @param type - The desired type, e.g. 'blob', 'tree', 'edit' + * @public + */ +export function replaceGitLabUrlType( url: string, type: 'blob' | 'tree' | 'edit', ): string { diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts index 950205d61c..e8d6665a6f 100644 --- a/packages/integration/src/gitlab/index.ts +++ b/packages/integration/src/gitlab/index.ts @@ -20,4 +20,4 @@ export { } from './config'; export type { GitLabIntegrationConfig } from './config'; export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; -export { GitLabIntegration } from './GitLabIntegration'; +export { GitLabIntegration, replaceGitLabUrlType } from './GitLabIntegration'; diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index 60a2789f05..06feb4104e 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BitbucketIntegration } from './bitbucket'; +import { BitbucketServerIntegration } from './bitbucketServer'; import { basicIntegrations, defaultScmResolveUrl, @@ -24,11 +24,11 @@ import { describe('basicIntegrations', () => { describe('byUrl', () => { it('handles hosts without a port', () => { - const integration = new BitbucketIntegration({ + const integration = new BitbucketServerIntegration({ host: 'host.com', apiBaseUrl: 'a', }); - const integrations = basicIntegrations( + const integrations = basicIntegrations( [integration], i => i.config.host, ); @@ -36,11 +36,11 @@ describe('basicIntegrations', () => { expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined(); }); it('handles hosts with a port', () => { - const integration = new BitbucketIntegration({ + const integration = new BitbucketServerIntegration({ host: 'host.com:8080', apiBaseUrl: 'a', }); - const integrations = basicIntegrations( + const integrations = basicIntegrations( [integration], i => i.config.host, ); diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index cf0eeca51a..388b874275 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -20,13 +20,15 @@ * @packageDocumentation */ +export * from './awsS3'; export * from './azure'; export * from './bitbucket'; +export * from './bitbucketCloud'; +export * from './bitbucketServer'; export * from './gerrit'; export * from './github'; export * from './gitlab'; export * from './googleGcs'; -export * from './awsS3'; export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; export type { IntegrationsByType } from './ScmIntegrations'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 5ab5d049fb..f4f759cfe9 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -17,7 +17,9 @@ import { ScmIntegration, ScmIntegrationsGroup } from './types'; import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; @@ -31,7 +33,12 @@ export interface ScmIntegrationRegistry extends ScmIntegrationsGroup { awsS3: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; + /** + * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` + */ bitbucket: ScmIntegrationsGroup; + bitbucketCloud: ScmIntegrationsGroup; + bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 9a4b005b2c..4557532194 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -36,7 +36,7 @@ "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/test-utils": "^1.0.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "msw": "^0.35.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 41e090f6fc..f24fb4ca9f 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/search-common +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + ## 0.3.3-next.1 ### Patch Changes diff --git a/packages/search-common/package.json b/packages/search-common/package.json index 58cf4d49cc..3fa096862f 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "No longer maintained. Use @backstage/plugin-search-common instead.", - "version": "0.3.3-next.1", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -35,7 +35,7 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/plugin-search-common": "0.3.3-next.1" + "@backstage/plugin-search-common": "0.3.3" }, "devDependencies": {}, "jest": { diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 3f80ade3e6..0fd55b8167 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,50 @@ # techdocs-cli-embedded-app +## 0.2.70-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-techdocs@1.1.1-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 + - @backstage/test-utils@1.0.2-next.0 + +## 0.2.69 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.1.0 + - @backstage/cli@0.17.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-app-api@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-techdocs-react@0.1.0 + - @backstage/integration-react@1.0.1 + - @backstage/test-utils@1.0.1 + - @backstage/plugin-catalog@1.1.0 + - @backstage/app-defaults@1.0.1 + +## 0.2.69-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.1.0-next.3 + - @backstage/cli@0.17.0-next.3 + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/test-utils@1.0.1-next.2 + - @backstage/plugin-catalog@1.1.0-next.3 + - @backstage/app-defaults@1.0.1-next.2 + - @backstage/plugin-techdocs-react@0.1.0-next.0 + ## 0.2.69-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 8771d5c14b..b356269248 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,23 +1,24 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.69-next.1", + "version": "0.2.70-next.0", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^1.0.1-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/app-defaults": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", "@backstage/config": "^1.0.0", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog": "^1.1.0-next.1", - "@backstage/plugin-techdocs": "^1.0.1-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/plugin-techdocs": "^1.1.1-next.0", + "@backstage/plugin-techdocs-react": "^0.1.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -29,7 +30,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/packages/techdocs-cli-embedded-app/src/App.tsx b/packages/techdocs-cli-embedded-app/src/App.tsx index 51bdfbbb11..194d26943d 100644 --- a/packages/techdocs-cli-embedded-app/src/App.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.tsx @@ -16,20 +16,27 @@ import React from 'react'; import { Navigate, Route } from 'react-router'; -import { createApp } from '@backstage/app-defaults'; -import { FlatRoutes } from '@backstage/core-app-api'; -import { CatalogEntityPage } from '@backstage/plugin-catalog'; import { DefaultTechDocsHome, TechDocsIndexPage, TechDocsReaderPage, + techdocsPlugin, } from '@backstage/plugin-techdocs'; +import { + createTechDocsAddonExtension, + TechDocsAddons, + TechDocsAddonLocations, +} from '@backstage/plugin-techdocs-react'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { CatalogEntityPage } from '@backstage/plugin-catalog'; + import { apis } from './apis'; -import { Root } from './components/Root'; -import { techDocsPage } from './components/TechDocsPage'; import * as plugins from './plugins'; import { configLoader } from './config'; +import { Root } from './components/Root'; +import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage'; const app = createApp({ apis, @@ -40,6 +47,14 @@ const app = createApp({ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); +const ThemeToggleAddon = techdocsPlugin.provide( + createTechDocsAddonExtension({ + name: 'ThemeToggleAddon', + component: TechDocsThemeToggle, + location: TechDocsAddonLocations.Header, + }), +); + const routes = ( @@ -56,6 +71,9 @@ const routes = ( element={} > {techDocsPage} + + + ); diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index 56686dd081..c7328a0487 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -14,29 +14,20 @@ * limitations under the License. */ -import React, { - FC, - createContext, - useContext, - useState, - useCallback, -} from 'react'; +import React, { useState } from 'react'; import { Theme, makeStyles } from '@material-ui/core'; -import { ThemeProvider, Box, Tooltip, IconButton } from '@material-ui/core'; +import { Box, Tooltip, IconButton } from '@material-ui/core'; import LightIcon from '@material-ui/icons/Brightness7'; import DarkIcon from '@material-ui/icons/Brightness4'; -import { lightTheme, darkTheme } from '@backstage/theme'; -import { CompoundEntityRef } from '@backstage/catalog-model'; - -import { Content } from '@backstage/core-components'; +import { appThemeApiRef, useApi } from '@backstage/core-plugin-api'; import { - Reader, TechDocsReaderPage, TechDocsReaderPageHeader, + TechDocsReaderPageContent, } from '@backstage/plugin-techdocs'; const useStyles = makeStyles((theme: Theme) => ({ @@ -60,44 +51,12 @@ enum Themes { DARK = 'dark', } -type TechDocsThemeValue = { - theme: Themes; - toggleTheme: () => void; -}; - -const TechDocsThemeContext = createContext({ - theme: Themes.LIGHT, - toggleTheme: () => {}, -}); - -const TechdocsThemeProvider: FC = ({ children }) => { - const [theme, setTheme] = useState(Themes.LIGHT); - - const toggleTheme = useCallback(() => { - setTheme(prevTheme => - prevTheme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT, - ); - }, [setTheme]); - - const value = { theme, toggleTheme }; - - const themes = { - [Themes.LIGHT]: lightTheme, - [Themes.DARK]: darkTheme, - }; - - return ( - - {children} - - ); -}; - -const useTechDocsTheme = () => useContext(TechDocsThemeContext); - -const TechDocsThemeToggle = () => { +export const TechDocsThemeToggle = () => { + const appThemeApi = useApi(appThemeApiRef); const classes = useStyles(); - const { theme, toggleTheme } = useTechDocsTheme(); + const [theme, setTheme] = useState( + (appThemeApi.getActiveThemeId() as Themes) || Themes.LIGHT, + ); const themes = { [Themes.LIGHT]: { @@ -112,10 +71,18 @@ const TechDocsThemeToggle = () => { const { title, icon: Icon } = themes[theme]; + const handleSetTheme = () => { + setTheme(prevTheme => { + const newTheme = prevTheme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT; + appThemeApi.setActiveThemeId(newTheme); + return newTheme; + }); + }; + return ( - + @@ -123,47 +90,13 @@ const TechDocsThemeToggle = () => { ); }; -const TechDocsPageContent = ({ - onReady, - entityRef, -}: { - entityRef: CompoundEntityRef; - onReady: () => void; -}) => { - const classes = useStyles(); - - return ( - - - - ); -}; - const DefaultTechDocsPage = () => { - const techDocsMetadata = { - site_name: 'Live preview environment', - site_description: '', - }; - return ( - {({ entityRef, onReady }) => ( - <> - - - - - - )} + + ); }; -export const techDocsPage = ( - - - -); +export const techDocsPage = ; diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 259a298d68..f7e3b9c006 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,56 @@ # @techdocs/cli +## 1.1.1-next.0 + +### Patch Changes + +- 344ea56acc: Bump `commander` to version 9.1.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 733187987b: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + + **WARNING**: If you notice 404s in TechDocs after updating, check to make sure that all markdown files referenced in your `mkdocs.yml`s' `nav` sections exist. The following flag may be passed to the `generate` command to temporarily revert to the broken behavior. + + ```sh + techdocs-cli generate --legacyCopyReadmeMdToIndexMd + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- eb470ea54c: Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-techdocs-node@1.1.0 + +## 1.1.0-next.1 + +### Minor Changes + +- bcf1a2496c: BREAKING: The default Techdocs behavior will no longer attempt to copy `docs/README.md` or `README.md` to `docs/index.md` (if not found). To retain this behavior in your instance, you can set the following config in your `app-config.yaml`: + + ```yaml + techdocs: + generator: + mkdocs: + legacyCopyReadmeMdToIndexMd: true + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- eb470ea54c: Adds a new flag to override the entrypoint when using a custom docker image. It could be used to reuse existing images with different entrypoints. +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + - @backstage/plugin-techdocs-node@1.1.0-next.2 + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 28d4b64ebb..be41a88c7e 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.0.1-next.0", + "version": "1.1.1-next.0", "private": false, "publishConfig": { "access": "public" @@ -37,7 +37,7 @@ "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -62,13 +62,13 @@ "ext": "ts" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/cli-common": "^0.1.8", "@backstage/config": "^1.0.0", - "@backstage/plugin-techdocs-node": "^1.0.1-next.1", + "@backstage/plugin-techdocs-node": "^1.1.1-next.0", "@types/dockerode": "^3.3.0", - "commander": "^6.1.0", + "commander": "^9.1.0", "dockerode": "^3.3.1", "fs-extra": "^10.0.1", "http-proxy": "^1.18.1", diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index d192e7a2f9..309b9c7969 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -15,7 +15,7 @@ */ import { resolve } from 'path'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import fs from 'fs-extra'; import Docker from 'dockerode'; import { @@ -30,19 +30,19 @@ import { } from '../../lib/utility'; import { stdout } from 'process'; -export default async function generate(cmd: Command) { +export default async function generate(opts: OptionValues) { // Use techdocs-node package to generate docs. Keep consistency between Backstage and CI generating docs. // Docs can be prepared using actions/checkout or git clone, or similar paradigms on CI. The TechDocs CI workflow // will run on the CI pipeline containing the documentation files. - const logger = createLogger({ verbose: cmd.verbose }); + const logger = createLogger({ verbose: opts.verbose }); - const sourceDir = resolve(cmd.sourceDir); - const outputDir = resolve(cmd.outputDir); - const omitTechdocsCorePlugin = cmd.omitTechdocsCoreMkdocsPlugin; - const dockerImage = cmd.dockerImage; - const pullImage = cmd.pull; - const legacyCopyReadmeMdToIndexMd = cmd.legacyCopyReadmeMdToIndexMd; + const sourceDir = resolve(opts.sourceDir); + const outputDir = resolve(opts.outputDir); + const omitTechdocsCorePlugin = opts.omitTechdocsCoreMkdocsPlugin; + const dockerImage = opts.dockerImage; + const pullImage = opts.pull; + const legacyCopyReadmeMdToIndexMd = opts.legacyCopyReadmeMdToIndexMd; logger.info(`Using source dir ${sourceDir}`); logger.info(`Will output generated files in ${outputDir}`); @@ -54,7 +54,7 @@ export default async function generate(cmd: Command) { const config = new ConfigReader({ techdocs: { generator: { - runIn: cmd.docker ? 'docker' : 'local', + runIn: opts.docker ? 'docker' : 'local', dockerImage, pullImage, legacyCopyReadmeMdToIndexMd, @@ -70,10 +70,10 @@ export default async function generate(cmd: Command) { const containerRunner = new DockerContainerRunner({ dockerClient }); let parsedLocationAnnotation = {} as ParsedLocationAnnotation; - if (cmd.techdocsRef) { + if (opts.techdocsRef) { try { parsedLocationAnnotation = convertTechDocsRefToLocationAnnotation( - cmd.techdocsRef, + opts.techdocsRef, ); } catch (err) { logger.error(err.message); @@ -91,13 +91,13 @@ export default async function generate(cmd: Command) { await techdocsGenerator.run({ inputDir: sourceDir, outputDir, - ...(cmd.techdocsRef + ...(opts.techdocsRef ? { parsedLocationAnnotation, } : {}), logger, - etag: cmd.etag, + etag: opts.etag, ...(process.env.LOG_LEVEL === 'debug' ? { logStream: stdout } : {}), }); diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index bad2f195ef..85a218eb54 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { CommanderStatic } from 'commander'; +import { Command } from 'commander'; import { TechdocsGenerator } from '@backstage/plugin-techdocs-node'; const defaultDockerImage = TechdocsGenerator.defaultDockerImage; -export function registerCommands(program: CommanderStatic) { +export function registerCommands(program: Command) { program .command('generate') .description('Generate TechDocs documentation site using MkDocs.') diff --git a/packages/techdocs-cli/src/commands/migrate/migrate.ts b/packages/techdocs-cli/src/commands/migrate/migrate.ts index 067bf1f173..a086073cda 100644 --- a/packages/techdocs-cli/src/commands/migrate/migrate.ts +++ b/packages/techdocs-cli/src/commands/migrate/migrate.ts @@ -16,19 +16,19 @@ import { SingleHostDiscovery } from '@backstage/backend-common'; import { Publisher } from '@backstage/plugin-techdocs-node'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { createLogger } from '../../lib/utility'; import { PublisherConfig } from '../../lib/PublisherConfig'; -export default async function migrate(cmd: Command) { - const logger = createLogger({ verbose: cmd.verbose }); +export default async function migrate(opts: OptionValues) { + const logger = createLogger({ verbose: opts.verbose }); - const config = PublisherConfig.getValidConfig(cmd); + const config = PublisherConfig.getValidConfig(opts); const discovery = SingleHostDiscovery.fromConfig(config); const publisher = await Publisher.fromConfig(config, { logger, discovery }); if (!publisher.migrateDocsCase) { - throw new Error(`Migration not implemented for ${cmd.publisherType}`); + throw new Error(`Migration not implemented for ${opts.publisherType}`); } // Check that the publisher's underlying storage is ready and available. @@ -39,12 +39,12 @@ export default async function migrate(cmd: Command) { } // Validate and parse migration arguments. - const removeOriginal = cmd.removeOriginal; - const numericConcurrency = parseInt(cmd.concurrency, 10); + const removeOriginal = opts.removeOriginal; + const numericConcurrency = parseInt(opts.concurrency, 10); if (!Number.isInteger(numericConcurrency) || numericConcurrency <= 0) { throw new Error( - `Concurrency must be a number greater than 1. ${cmd.concurrency} provided.`, + `Concurrency must be a number greater than 1. ${opts.concurrency} provided.`, ); } diff --git a/packages/techdocs-cli/src/commands/publish/publish.ts b/packages/techdocs-cli/src/commands/publish/publish.ts index c84180b032..f4e78b57a4 100644 --- a/packages/techdocs-cli/src/commands/publish/publish.ts +++ b/packages/techdocs-cli/src/commands/publish/publish.ts @@ -15,17 +15,17 @@ */ import { resolve } from 'path'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { createLogger } from '../../lib/utility'; import { SingleHostDiscovery } from '@backstage/backend-common'; import { Publisher } from '@backstage/plugin-techdocs-node'; import { Entity } from '@backstage/catalog-model'; import { PublisherConfig } from '../../lib/PublisherConfig'; -export default async function publish(cmd: Command): Promise { - const logger = createLogger({ verbose: cmd.verbose }); +export default async function publish(opts: OptionValues): Promise { + const logger = createLogger({ verbose: opts.verbose }); - const config = PublisherConfig.getValidConfig(cmd); + const config = PublisherConfig.getValidConfig(opts); const discovery = SingleHostDiscovery.fromConfig(config); const publisher = await Publisher.fromConfig(config, { logger, discovery }); @@ -36,7 +36,7 @@ export default async function publish(cmd: Command): Promise { return Promise.reject(new Error('')); } - const [namespace, kind, name] = cmd.entity.split('/'); + const [namespace, kind, name] = opts.entity.split('/'); const entity = { kind, metadata: { @@ -45,7 +45,7 @@ export default async function publish(cmd: Command): Promise { }, } as Entity; - const directory = resolve(cmd.directory); + const directory = resolve(opts.directory); await publisher.publish({ entity, directory }); return true; diff --git a/packages/techdocs-cli/src/commands/serve/mkdocs.ts b/packages/techdocs-cli/src/commands/serve/mkdocs.ts index 6a0952b945..511df00a17 100644 --- a/packages/techdocs-cli/src/commands/serve/mkdocs.ts +++ b/packages/techdocs-cli/src/commands/serve/mkdocs.ts @@ -14,25 +14,25 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import openBrowser from 'react-dev-utils/openBrowser'; import { createLogger } from '../../lib/utility'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; -export default async function serveMkdocs(cmd: Command) { - const logger = createLogger({ verbose: cmd.verbose }); +export default async function serveMkdocs(opts: OptionValues) { + const logger = createLogger({ verbose: opts.verbose }); - const dockerAddr = `http://0.0.0.0:${cmd.port}`; - const localAddr = `http://127.0.0.1:${cmd.port}`; - const expectedDevAddr = cmd.docker ? dockerAddr : localAddr; + const dockerAddr = `http://0.0.0.0:${opts.port}`; + const localAddr = `http://127.0.0.1:${opts.port}`; + const expectedDevAddr = opts.docker ? dockerAddr : localAddr; // We want to open browser only once based on a log. let boolOpenBrowserTriggered = false; const logFunc: LogFunc = data => { // Sometimes the lines contain an unnecessary extra new line in between const logLines = data.toString().split('\n'); - const logPrefix = cmd.docker ? '[docker/mkdocs]' : '[mkdocs]'; + const logPrefix = opts.docker ? '[docker/mkdocs]' : '[mkdocs]'; logLines.forEach(line => { if (line === '') { return; @@ -59,10 +59,10 @@ export default async function serveMkdocs(cmd: Command) { // Commander stores --no-docker in cmd.docker variable const childProcess = await runMkdocsServer({ - port: cmd.port, - dockerImage: cmd.dockerImage, - dockerEntrypoint: cmd.dockerEntrypoint, - useDocker: cmd.docker, + port: opts.port, + dockerImage: opts.dockerImage, + dockerEntrypoint: opts.dockerEntrypoint, + useDocker: opts.docker, stdoutLogFunc: logFunc, stderrLogFunc: logFunc, }); diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index c2ac6d8105..454e9ffa55 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import path from 'path'; import openBrowser from 'react-dev-utils/openBrowser'; import { findPaths } from '@backstage/cli-common'; @@ -42,8 +42,8 @@ function findPreviewBundlePath(): string { } } -export default async function serve(cmd: Command) { - const logger = createLogger({ verbose: cmd.verbose }); +export default async function serve(opts: OptionValues) { + const logger = createLogger({ verbose: opts.verbose }); // Determine if we want to run in local dev mode or not // This will run the backstage http server on a different port and only used @@ -58,15 +58,17 @@ export default async function serve(cmd: Command) { const backstagePort = 3000; const backstageBackendPort = 7007; - const mkdocsDockerAddr = `http://0.0.0.0:${cmd.mkdocsPort}`; - const mkdocsLocalAddr = `http://127.0.0.1:${cmd.mkdocsPort}`; - const mkdocsExpectedDevAddr = cmd.docker ? mkdocsDockerAddr : mkdocsLocalAddr; + const mkdocsDockerAddr = `http://0.0.0.0:${opts.mkdocsPort}`; + const mkdocsLocalAddr = `http://127.0.0.1:${opts.mkdocsPort}`; + const mkdocsExpectedDevAddr = opts.docker + ? mkdocsDockerAddr + : mkdocsLocalAddr; let mkdocsServerHasStarted = false; const mkdocsLogFunc: LogFunc = data => { // Sometimes the lines contain an unnecessary extra new line const logLines = data.toString().split('\n'); - const logPrefix = cmd.docker ? '[docker/mkdocs]' : '[mkdocs]'; + const logPrefix = opts.docker ? '[docker/mkdocs]' : '[mkdocs]'; logLines.forEach(line => { if (line === '') { return; @@ -88,10 +90,10 @@ export default async function serve(cmd: Command) { // Had me questioning this whole implementation for half an hour. logger.info('Starting mkdocs server.'); const mkdocsChildProcess = await runMkdocsServer({ - port: cmd.mkdocsPort, - dockerImage: cmd.dockerImage, - dockerEntrypoint: cmd.dockerEntrypoint, - useDocker: cmd.docker, + port: opts.mkdocsPort, + dockerImage: opts.dockerImage, + dockerEntrypoint: opts.dockerEntrypoint, + useDocker: opts.docker, stdoutLogFunc: mkdocsLogFunc, stderrLogFunc: mkdocsLogFunc, }); @@ -116,8 +118,8 @@ export default async function serve(cmd: Command) { const httpServer = new HTTPServer( findPreviewBundlePath(), port, - cmd.mkdocsPort, - cmd.verbose, + opts.mkdocsPort, + opts.verbose, ); httpServer diff --git a/packages/techdocs-cli/src/index.ts b/packages/techdocs-cli/src/index.ts index 6f4437cbce..ff93dada85 100644 --- a/packages/techdocs-cli/src/index.ts +++ b/packages/techdocs-cli/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import program from 'commander'; +import { program } from 'commander'; import { registerCommands } from './commands'; import { version } from '../package.json'; diff --git a/packages/techdocs-cli/src/lib/PublisherConfig.test.ts b/packages/techdocs-cli/src/lib/PublisherConfig.test.ts index 88f542c049..24cad332ac 100644 --- a/packages/techdocs-cli/src/lib/PublisherConfig.test.ts +++ b/packages/techdocs-cli/src/lib/PublisherConfig.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Command } from 'commander'; +import { OptionValues } from 'commander'; import { PublisherConfig } from './PublisherConfig'; describe('getValidPublisherConfig', () => { it('should not allow unknown publisher types', () => { const invalidConfig = { publisherType: 'unknown publisher', - } as unknown as Command; + } as unknown as OptionValues; expect(() => PublisherConfig.getValidConfig(invalidConfig)).toThrowError( `Unknown publisher type ${invalidConfig.publisherType}`, @@ -32,7 +32,7 @@ describe('getValidPublisherConfig', () => { it('should require --azureAccountName', () => { const config = { publisherType: 'azureBlobStorage', - } as unknown as Command; + } as unknown as OptionValues; expect(() => PublisherConfig.getValidConfig(config)).toThrowError( 'azureBlobStorage requires --azureAccountName to be specified', @@ -44,7 +44,7 @@ describe('getValidPublisherConfig', () => { publisherType: 'azureBlobStorage', azureAccountName: 'someAccountName', storageName: 'someContainer', - } as unknown as Command; + } as unknown as OptionValues; const actualConfig = PublisherConfig.getValidConfig(config); expect(actualConfig.getString('techdocs.publisher.type')).toBe( @@ -69,7 +69,7 @@ describe('getValidPublisherConfig', () => { publisherType: 'awsS3', storageName: 'someStorageName', awsBucketRootPath: 'backstage-data/techdocs', - } as unknown as Command; + } as unknown as OptionValues; const actualConfig = PublisherConfig.getValidConfig(config); expect(actualConfig.getString('techdocs.publisher.type')).toBe('awsS3'); @@ -86,7 +86,7 @@ describe('getValidPublisherConfig', () => { publisherType: 'awsS3', storageName: 'someStorageName', awsS3sse: 'aws:kms', - } as unknown as Command; + } as unknown as OptionValues; const actualConfig = PublisherConfig.getValidConfig(config); expect(actualConfig.getString('techdocs.publisher.type')).toBe('awsS3'); @@ -102,7 +102,7 @@ describe('getValidPublisherConfig', () => { publisherType: 'openStackSwift', osCredentialId: 'someCredentialId', osSecret: 'someSecret', - } as unknown as Command; + } as unknown as OptionValues; expect(() => PublisherConfig.getValidConfig(config)).toThrowError( `openStackSwift requires the following params to be specified: ${[ @@ -120,7 +120,7 @@ describe('getValidPublisherConfig', () => { osSecret: 'someSecret', osAuthUrl: 'someAuthUrl', osSwiftUrl: 'someSwiftUrl', - } as unknown as Command; + } as unknown as OptionValues; const actualConfig = PublisherConfig.getValidConfig(config); expect(actualConfig.getString('techdocs.publisher.type')).toBe( @@ -146,7 +146,7 @@ describe('getValidPublisherConfig', () => { publisherType: 'googleGcs', storageName: 'someStorageName', gcsBucketRootPath: 'backstage-data/techdocs', - } as unknown as Command; + } as unknown as OptionValues; const actualConfig = PublisherConfig.getValidConfig(config); expect(actualConfig.getString('techdocs.publisher.type')).toBe( diff --git a/packages/techdocs-cli/src/lib/PublisherConfig.ts b/packages/techdocs-cli/src/lib/PublisherConfig.ts index c18b9ff588..0d1c8fc8a9 100644 --- a/packages/techdocs-cli/src/lib/PublisherConfig.ts +++ b/packages/techdocs-cli/src/lib/PublisherConfig.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { Command } from 'commander'; +import { OptionValues } from 'commander'; type Publisher = keyof typeof PublisherConfig['configFactories']; type PublisherConfiguration = { @@ -45,11 +45,11 @@ export class PublisherConfig { * Note: This assumes that proper credentials are set in Environment * variables for the respective GCS/AWS clients to work. */ - static getValidConfig(cmd: Command): ConfigReader { - const publisherType = cmd.publisherType; + static getValidConfig(opts: OptionValues): ConfigReader { + const publisherType = opts.publisherType; if (!PublisherConfig.isKnownPublisher(publisherType)) { - throw new Error(`Unknown publisher type ${cmd.publisherType}`); + throw new Error(`Unknown publisher type ${opts.publisherType}`); } return new ConfigReader({ @@ -61,9 +61,9 @@ export class PublisherConfig { }, }, techdocs: { - publisher: PublisherConfig.configFactories[publisherType](cmd), + publisher: PublisherConfig.configFactories[publisherType](opts), legacyUseCaseSensitiveTripletPaths: - cmd.legacyUseCaseSensitiveTripletPaths, + opts.legacyUseCaseSensitiveTripletPaths, }, }); } @@ -80,16 +80,20 @@ export class PublisherConfig { /** * Retrieve valid AWS S3 configuration based on the command. */ - private static getValidAwsS3Config(cmd: Command): PublisherConfiguration { + private static getValidAwsS3Config( + opts: OptionValues, + ): PublisherConfiguration { return { type: 'awsS3', awsS3: { - bucketName: cmd.storageName, - ...(cmd.awsBucketRootPath && { bucketRootPath: cmd.awsBucketRootPath }), - ...(cmd.awsRoleArn && { credentials: { roleArn: cmd.awsRoleArn } }), - ...(cmd.awsEndpoint && { endpoint: cmd.awsEndpoint }), - ...(cmd.awsS3ForcePathStyle && { s3ForcePathStyle: true }), - ...(cmd.awsS3sse && { sse: cmd.awsS3sse }), + bucketName: opts.storageName, + ...(opts.awsBucketRootPath && { + bucketRootPath: opts.awsBucketRootPath, + }), + ...(opts.awsRoleArn && { credentials: { roleArn: opts.awsRoleArn } }), + ...(opts.awsEndpoint && { endpoint: opts.awsEndpoint }), + ...(opts.awsS3ForcePathStyle && { s3ForcePathStyle: true }), + ...(opts.awsS3sse && { sse: opts.awsS3sse }), }, }; } @@ -97,8 +101,10 @@ export class PublisherConfig { /** * Retrieve valid Azure Blob Storage configuration based on the command. */ - private static getValidAzureConfig(cmd: Command): PublisherConfiguration { - if (!cmd.azureAccountName) { + private static getValidAzureConfig( + opts: OptionValues, + ): PublisherConfiguration { + if (!opts.azureAccountName) { throw new Error( `azureBlobStorage requires --azureAccountName to be specified`, ); @@ -107,10 +113,10 @@ export class PublisherConfig { return { type: 'azureBlobStorage', azureBlobStorage: { - containerName: cmd.storageName, + containerName: opts.storageName, credentials: { - accountName: cmd.azureAccountName, - accountKey: cmd.azureAccountKey, + accountName: opts.azureAccountName, + accountKey: opts.azureAccountKey, }, }, }; @@ -119,12 +125,16 @@ export class PublisherConfig { /** * Retrieve valid GCS configuration based on the command. */ - private static getValidGoogleGcsConfig(cmd: Command): PublisherConfiguration { + private static getValidGoogleGcsConfig( + opts: OptionValues, + ): PublisherConfiguration { return { type: 'googleGcs', googleGcs: { - bucketName: cmd.storageName, - ...(cmd.gcsBucketRootPath && { bucketRootPath: cmd.gcsBucketRootPath }), + bucketName: opts.storageName, + ...(opts.gcsBucketRootPath && { + bucketRootPath: opts.gcsBucketRootPath, + }), }, }; } @@ -133,14 +143,14 @@ export class PublisherConfig { * Retrieves valid OpenStack Swift configuration based on the command. */ private static getValidOpenStackSwiftConfig( - cmd: Command, + opts: OptionValues, ): PublisherConfiguration { const missingParams = [ 'osCredentialId', 'osSecret', 'osAuthUrl', 'osSwiftUrl', - ].filter((param: string) => !cmd[param]); + ].filter((param: string) => !opts[param]); if (missingParams.length) { throw new Error( @@ -153,13 +163,13 @@ export class PublisherConfig { return { type: 'openStackSwift', openStackSwift: { - containerName: cmd.storageName, + containerName: opts.storageName, credentials: { - id: cmd.osCredentialId, - secret: cmd.osSecret, + id: opts.osCredentialId, + secret: opts.osSecret, }, - authUrl: cmd.osAuthUrl, - swiftUrl: cmd.osSwiftUrl, + authUrl: opts.osAuthUrl, + swiftUrl: opts.osSwiftUrl, }, }; } diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 145b2adb9d..8d3f7f4fde 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/techdocs-common +## 0.11.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## 0.11.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.1.0 + +## 0.11.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.1.0-next.2 + ## 0.11.14-next.1 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 3c2dc8bcf9..aecd9622e2 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "No longer maintained. Use @backstage/plugin-techdocs-node instead.", - "version": "0.11.14-next.1", + "version": "0.11.15-next.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -36,7 +36,7 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/plugin-techdocs-node": "1.0.1-next.1" + "@backstage/plugin-techdocs-node": "1.1.1-next.0" }, "devDependencies": {}, "jest": { diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index f2561ec416..c7453f6dda 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/test-utils +## 1.0.2-next.0 + +### Patch Changes + +- 7a5ddfd595: Added missing `Routes` element to wrap the `Route` elements of the test app wrapping. +- 7a5ddfd595: The internal elements created as part of the `mountedRoutes` implementation are now hidden during rendering. + +## 1.0.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/core-app-api@1.0.1 + - @backstage/core-plugin-api@1.0.1 + +## 1.0.1-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/plugin-permission-react@0.4.0-next.1 + ## 1.0.1-next.1 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index fc85ec4606..a924d51541 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.0.1-next.1", + "version": "1.0.2-next.0", "private": false, "publishConfig": { "access": "public", @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-react": "^0.4.0-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-react": "^0.4.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", @@ -55,7 +55,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", "msw": "^0.35.0" diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 32ca3dfc02..c59f66aab5 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -159,4 +159,17 @@ describe('wrapInTestApp', () => { expect(rendered.getByText('Link S: /my-b-path/y/p')).toBeInTheDocument(); expect(rendered.getByText('Link E: /my-e-path/z')).toBeInTheDocument(); }); + + it('should not make route mounting elements visible during tests', async () => { + const routeRef = createRouteRef({ id: 'foo' }); + + const rendered = await renderInTestApp(foo, { + mountedRoutes: { '/foo': routeRef }, + }); + + const [root] = rendered.baseElement.children; + expect(root).toBeInTheDocument(); + expect(root.children.length).toBe(1); + expect(root.children[0].textContent).toBe('foo'); + }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index cad1df4ecc..b12e8935b6 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType, ReactNode, ReactElement } from 'react'; -import { MemoryRouter } from 'react-router'; +import { MemoryRouter, Routes } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core/styles'; @@ -70,6 +70,8 @@ const BootErrorPage = ({ step, error }: BootErrorPageProps) => { }; const Progress = () =>
; +const NoRender = (_props: { children: ReactNode }) => null; + /** * Options to customize the behavior of the test app wrapper. * @public @@ -190,10 +192,12 @@ export function wrapInTestApp( return ( - {routeElements} + {routeElements} {/* The path of * here is needed to be set as a catch all, so it will render the wrapper element * and work with nested routes if they exist too */} - + + + ); diff --git a/packages/theme/package.json b/packages/theme/package.json index 5bcfd328e0..537b6bab96 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -36,7 +36,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/packages/types/package.json b/packages/types/package.json index 2c2aba560f..112247fefe 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -34,7 +34,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/CHANGELOG.md b/packages/version-bridge/CHANGELOG.md index e814333637..c3c3a9a57f 100644 --- a/packages/version-bridge/CHANGELOG.md +++ b/packages/version-bridge/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/version-bridge +## 1.0.1 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 + ## 1.0.0 ### Major Changes diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index a03e3d9f75..c7fdf46951 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/version-bridge", "description": "Utilities used by @backstage packages to support multiple concurrent versions", - "version": "1.0.0", + "version": "1.0.1", "private": false, "publishConfig": { "access": "public", @@ -37,10 +37,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2" + "@testing-library/react-hooks": "^8.0.0" }, "files": [ "dist" diff --git a/plugins/adr-backend/.eslintrc.js b/plugins/adr-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md new file mode 100644 index 0000000000..79b5fe13ed --- /dev/null +++ b/plugins/adr-backend/README.md @@ -0,0 +1,47 @@ +# ADR Backend + +This ADR backend plugin is primarily responsible for the following: + +- Provides a `DefaultAdrCollatorFactory`, which can be used in the search backend to index ADR documents associated with entities to your Backstage Search. + +## Indexing ADR documents for search + +Before you are able to start indexing ADR documents to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). + +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, install this plugin and add the following code snippet to add the `DefaultAdrCollatorFactory`. Also make sure to set up the frontend [ADR plugin](../adr/README.md) so search results can be routed correctly. + +```bash +# From your Backstage root directory +yarn --cwd packages/backend add @backstage/plugin-adr-backend +``` + +```ts +import { DefaultAdrCollatorFactory } from '@backstage/plugin-adr-backend'; + +... + +indexBuilder.addCollator({ + schedule, + factory: DefaultAdrCollatorFactory.fromConfig({ + cache: env.cache, + config: env.config, + discovery: env.discovery, + logger: env.logger, + reader: env.reader, + tokenManager: env.tokenManager, + }), +}); +``` + +### Parsing custom ADR document formats + +By default, the `DefaultAdrCollatorFactory` will parse and index documents that follow the [MADR](https://adr.github.io/madr/) standard file name and template format. If you use a different ADR format and file name convention, you can configure `DefaultAdrCollatorFactory` with custom `adrFilePathFilterFn` and `parser` options (see type definitions for details): + +```ts +DefaultAdrCollatorFactory.fromConfig({ + ... + parser: myCustomAdrParser, + adrFilePathFilterFn: myCustomAdrFilePathFilter, + ... +}) +``` diff --git a/plugins/adr-backend/api-report.md b/plugins/adr-backend/api-report.md new file mode 100644 index 0000000000..5a671e9c8f --- /dev/null +++ b/plugins/adr-backend/api-report.md @@ -0,0 +1,66 @@ +## API Report File for "@backstage/plugin-adr-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { AdrDocument } from '@backstage/plugin-adr-common'; +import { AdrFilePathFilterFn } from '@backstage/plugin-adr-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { Entity } from '@backstage/catalog-model'; +import { Logger } from 'winston'; +import { PluginCacheManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; +import { TokenManager } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; + +// @public +export type AdrCollatorFactoryOptions = { + adrFilePathFilterFn?: AdrFilePathFilterFn; + cache: PluginCacheManager; + config: Config; + catalogClient?: CatalogApi; + discovery: PluginEndpointDiscovery; + logger: Logger; + parser?: AdrParser; + reader: UrlReader; + tokenManager: TokenManager; +}; + +// @public +export type AdrParser = (ctx: AdrParserContext) => Promise; + +// @public +export type AdrParserContext = { + entity: Entity; + content: string; + path: string; +}; + +// @public +export const createMadrParser: (options?: MadrParserOptions) => AdrParser; + +// @public +export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + execute(): AsyncGenerator; + // (undocumented) + static fromConfig( + options: AdrCollatorFactoryOptions, + ): DefaultAdrCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; +} + +// @public +export type MadrParserOptions = { + locationTemplate?: string; + dateFormat?: string; +}; +``` diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json new file mode 100644 index 0000000000..e26d27ccee --- /dev/null +++ b/plugins/adr-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-adr-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/adr-backend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/config": "^1.0.0", + "@backstage/errors": "^1.0.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-adr-common": "^0.0.0", + "@backstage/plugin-search-common": "^0.3.3", + "luxon": "^2.0.2", + "marked": "^4.0.14", + "winston": "^3.2.1", + "node-fetch": "^2.6.5", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@types/marked": "^4.0.0", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr-backend/src/index.ts b/plugins/adr-backend/src/index.ts new file mode 100644 index 0000000000..86e07943df --- /dev/null +++ b/plugins/adr-backend/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * ADR backend plugin + * + * @packageDocumentation + */ + +export * from './search'; diff --git a/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts b/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts new file mode 100644 index 0000000000..e0c90af767 --- /dev/null +++ b/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts @@ -0,0 +1,212 @@ +/* + * 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 { Readable } from 'stream'; +import { Logger } from 'winston'; +import { + CacheClient, + PluginCacheManager, + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { NotModifiedError } from '@backstage/errors'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; +import { + AdrDocument, + AdrFilePathFilterFn, + ANNOTATION_ADR_LOCATION, + getAdrLocationUrl, + madrFilePathFilter, +} from '@backstage/plugin-adr-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; + +import { createMadrParser } from './createMadrParser'; +import { AdrParser } from './types'; + +/** + * Options to configure the AdrCollatorFactory + * @public + */ +export type AdrCollatorFactoryOptions = { + /** + * Function used to filter ADR file paths. + * Defaults to filtering paths following MADR filename format. + */ + adrFilePathFilterFn?: AdrFilePathFilterFn; + /** + * Plugin cache manager + */ + cache: PluginCacheManager; + /** + * App Config + */ + config: Config; + /** + * Catalog API client. Defaults to CatalogClient. + */ + catalogClient?: CatalogApi; + /** + * Plugin Endpoint Discovery client + */ + discovery: PluginEndpointDiscovery; + /** + * Logger + */ + logger: Logger; + /** + * ADR content parser. Defaults to built in MADR parser. + */ + parser?: AdrParser; + /** + * URL Reader + */ + reader: UrlReader; + /** + * Token Manager + */ + tokenManager: TokenManager; +}; + +/** + * Default collator to index ADR documents for Backstage search. + * @public + */ +export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'adr'; + private readonly adrFilePathFilterFn: AdrFilePathFilterFn; + private readonly cacheClient: CacheClient; + private readonly catalogClient: CatalogApi; + private readonly logger: Logger; + private readonly parser: AdrParser; + private readonly reader: UrlReader; + private readonly scmIntegrations: ScmIntegrationRegistry; + private readonly tokenManager: TokenManager; + + private constructor(options: AdrCollatorFactoryOptions) { + this.adrFilePathFilterFn = + options.adrFilePathFilterFn ?? madrFilePathFilter; + this.cacheClient = options.cache.getClient(); + this.catalogClient = + options.catalogClient ?? + new CatalogClient({ discoveryApi: options.discovery }); + this.logger = options.logger; + this.parser = options.parser ?? createMadrParser(); + this.reader = options.reader; + this.scmIntegrations = ScmIntegrations.fromConfig(options.config); + this.tokenManager = options.tokenManager; + } + + static fromConfig(options: AdrCollatorFactoryOptions) { + return new DefaultAdrCollatorFactory(options); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + const entities = ( + await this.catalogClient.getEntities( + { + filter: { + [`metadata.annotations.${ANNOTATION_ADR_LOCATION}`]: + CATALOG_FILTER_EXISTS, + }, + fields: [ + 'kind', + 'metadata.annotations', + 'metadata.name', + 'metadata.namespace', + ], + }, + { token }, + ) + ).items; + + for (const ent of entities) { + let adrsUrl: string; + try { + adrsUrl = getAdrLocationUrl(ent, this.scmIntegrations); + } catch (e: any) { + this.logger.error( + `Unable to get ADR location URL for ${stringifyEntityRef(ent)}: ${ + e.message + }`, + ); + continue; + } + + const cacheItem = (await this.cacheClient.get(adrsUrl)) as { + adrFiles: { + path: string; + content: string; + }[]; + etag: string; + }; + + let adrFiles = cacheItem?.adrFiles; + try { + const tree = await this.reader.readTree(adrsUrl, { + etag: cacheItem?.etag, + filter: filePath => this.adrFilePathFilterFn(filePath), + }); + + adrFiles = await Promise.all( + ( + await tree.files() + ).map(async f => ({ + path: f.path, + content: (await f.content()).toString('utf8'), + })), + ); + + await this.cacheClient.set(adrsUrl, { adrFiles, etag: tree.etag }); + } catch (error: any) { + // Ignore error if we're able to use cached response + if (!cacheItem || error.name !== NotModifiedError.name) { + throw error; + } + } + + for (const { content, path } of adrFiles) { + try { + const adrDoc = await this.parser({ + entity: ent, + path, + content, + }); + yield adrDoc; + } catch (e: any) { + this.logger.error(`Unable to parse ADR ${path}: ${e.message}`); + } + } + + this.logger.info(`Indexed ${adrFiles.length} ADRs from ${adrsUrl}`); + } + } +} diff --git a/plugins/adr-backend/src/search/createMadrParser.ts b/plugins/adr-backend/src/search/createMadrParser.ts new file mode 100644 index 0000000000..9efd49754a --- /dev/null +++ b/plugins/adr-backend/src/search/createMadrParser.ts @@ -0,0 +1,112 @@ +/* + * 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 { DateTime } from 'luxon'; +import { marked } from 'marked'; +import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; + +import { AdrParser } from './types'; + +const applyArgsToFormat = ( + format: string, + args: Record, +): string => { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted.toLowerCase(); +}; + +const DEFAULT_LOCATION_TEMPLATE = + '/catalog/:namespace/:kind/:name/adrs?record=:record'; + +/** + * + * Options for the default MADR content parser + * @public + */ +export type MadrParserOptions = { + /** + * Location template for the route of the frontend plugin + * Defaults to '/catalog/:namespace/:kind/:name/adrs?record=:record' + */ + locationTemplate?: string; + /** + * luxon DateTime format string to parse ADR dates with. + * Defaults to 'yyyy-MM-dd' + */ + dateFormat?: string; +}; + +/** + * Default content parser for ADRs following the MADR template (https://adr.github.io/madr/) + * @public + */ +export const createMadrParser = ( + options: MadrParserOptions = {}, +): AdrParser => { + const locationTemplate = + options.locationTemplate ?? DEFAULT_LOCATION_TEMPLATE; + const dateFormat = options.dateFormat ?? MADR_DATE_FORMAT; + + return async ({ entity, content, path }) => { + const tokens = marked.lexer(content); + if (!tokens.length) { + throw new Error('ADR has no content'); + } + + // First h1 header should contain ADR title + const adrTitle = ( + tokens.find( + t => t.type === 'heading' && t.depth === 1, + ) as marked.Tokens.Heading + )?.text; + + // First list should contain status & date metadata (if defined) + const listTokens = ( + tokens.find(t => t.type === 'list') as marked.Tokens.List + )?.items; + const adrStatus = listTokens + .find(t => /^status:/i.test(t.text)) + ?.text.replace(/^status:/i, '') + .trim() + .toLocaleLowerCase('en-US'); + const adrDateTime = DateTime.fromFormat( + listTokens + .find(t => /^date:/i.test(t.text)) + ?.text.replace(/^date:/i, '') + .trim() ?? '', + dateFormat, + ); + const adrDate = adrDateTime.isValid + ? adrDateTime.toFormat(MADR_DATE_FORMAT) + : undefined; + + return { + title: adrTitle ?? path.replace(/\.md$/, ''), + text: content, + status: adrStatus, + date: adrDate, + location: applyArgsToFormat(locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + record: path, + }), + }; + }; +}; diff --git a/plugins/adr-backend/src/search/index.ts b/plugins/adr-backend/src/search/index.ts new file mode 100644 index 0000000000..33f14631ec --- /dev/null +++ b/plugins/adr-backend/src/search/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './DefaultAdrCollatorFactory'; +export * from './createMadrParser'; +export * from './types'; diff --git a/plugins/adr-backend/src/search/types.ts b/plugins/adr-backend/src/search/types.ts new file mode 100644 index 0000000000..8ac47d5c8b --- /dev/null +++ b/plugins/adr-backend/src/search/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { AdrDocument } from '@backstage/plugin-adr-common'; + +/** + * Context passed to a AdrParser. + * @public + */ +export type AdrParserContext = { + /** + * The entity associated with the ADR. + */ + entity: Entity; + /** + * The ADR content string. + */ + content: string; + /** + * The ADR file path. + */ + path: string; +}; + +/** + * ADR parser function type. + * @public + */ +export type AdrParser = (ctx: AdrParserContext) => Promise; diff --git a/plugins/adr-common/.eslintrc.js b/plugins/adr-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr-common/README.md b/plugins/adr-common/README.md new file mode 100644 index 0000000000..38b5b7433c --- /dev/null +++ b/plugins/adr-common/README.md @@ -0,0 +1,3 @@ +# ADR Common + +Common types and functionalities for the ADR plugin. diff --git a/plugins/adr-common/api-report.md b/plugins/adr-common/api-report.md new file mode 100644 index 0000000000..290d984181 --- /dev/null +++ b/plugins/adr-common/api-report.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-adr-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +// @public +export interface AdrDocument extends IndexableDocument { + date?: string; + status?: string; +} + +// @public +export type AdrFilePathFilterFn = (path: string) => boolean; + +// @public +export const ANNOTATION_ADR_LOCATION = 'backstage.io/adr-location'; + +// @public +export const getAdrLocationUrl: ( + entity: Entity, + scmIntegration: ScmIntegrationRegistry, +) => string; + +// @public +export const isAdrAvailable: (entity: Entity) => boolean; + +// @public +export const MADR_DATE_FORMAT = 'yyyy-MM-dd'; + +// @public +export const madrFilePathFilter: AdrFilePathFilterFn; +``` diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json new file mode 100644 index 0000000000..41e28423dc --- /dev/null +++ b/plugins/adr-common/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/plugin-adr-common", + "description": "Common functionalities for the adr plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/adr-common" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-search-common": "^0.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts new file mode 100644 index 0000000000..94b6b788c8 --- /dev/null +++ b/plugins/adr-common/src/index.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ +/** + * Common types and functionalities for the ADR plugin. + * @packageDocumentation + */ +import { Entity, getEntitySourceLocation } from '@backstage/catalog-model'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +/** + * ADR plugin annotation. + * @public + */ +export const ANNOTATION_ADR_LOCATION = 'backstage.io/adr-location'; + +/** + * Standard luxon DateTime format string for MADR dates. + * @public + */ +export const MADR_DATE_FORMAT = 'yyyy-MM-dd'; + +/** + * Utility function to get the value of an entity ADR annotation. + * @public + */ +const getAdrLocationDir = (entity: Entity) => + entity.metadata.annotations?.[ANNOTATION_ADR_LOCATION]?.trim(); + +/** + * Utility function to determine if the given entity has ADRs. + * @public + */ +export const isAdrAvailable = (entity: Entity) => + Boolean(getAdrLocationDir(entity)); + +/** + * Utility function to extract the ADR location URL from an entity based off + * its ADR annotation and relative to the entity source location. + * @public + */ +export const getAdrLocationUrl = ( + entity: Entity, + scmIntegration: ScmIntegrationRegistry, +) => { + if (!isAdrAvailable(entity)) { + throw new Error(`Missing ADR annotation: ${ANNOTATION_ADR_LOCATION}`); + } + + return scmIntegration.resolveUrl({ + url: getAdrLocationDir(entity)!, + base: getEntitySourceLocation(entity).target, + }); +}; + +/** + * File path filter function type for ADR filenames + * @public + */ +export type AdrFilePathFilterFn = (path: string) => boolean; + +/** + * File path filter for MADR filename formats + * @public + */ +export const madrFilePathFilter: AdrFilePathFilterFn = (path: string) => + /^\d{4}-.+\.md$/.test(path); + +/** + * ADR indexable document interface + * @public + */ +export interface AdrDocument extends IndexableDocument { + /** + * ADR status label + */ + status?: string; + /** + * ADR date + */ + date?: string; +} diff --git a/plugins/adr/.eslintrc.js b/plugins/adr/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr/README.md b/plugins/adr/README.md new file mode 100644 index 0000000000..2888ff2f07 --- /dev/null +++ b/plugins/adr/README.md @@ -0,0 +1,126 @@ +# Architecture Decision Records (ADR) Plugin + +Welcome to the ADR plugin! + +This plugin allows you to browse ADRs associated with your entities as well as a way to discover ADRs across others entities via Backstage Search. Use this to learn from the past experience of other projects to guide your own architecture decisions. + +NOTE: This plugin currently only supports entities/ADRs registered via GitHub integration. + +## Setup + +Install this plugin: + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-adr +``` + +### Entity Pages + +1. Add the plugin as a tab to your Entity pages: + +```jsx +// In packages/app/src/components/catalog/EntityPage.tsx +import { EntityAdrContent, isAdrAvailable } from '@backstage/plugin-adr'; + +... + +const serviceEntityPage = ( + + {/* other tabs... */} + + + + +``` + +2. Add `backstage.io/adr-location` annotation to your `catalog-info.yaml`: + +```yaml +metadata: + annotations: + backstage.io/adr-location: +``` + +The value for `backstage.io/adr-location` should be a path relative to your `catalog-info.yaml` file or a absolute URL to the directory which contains your ADR markdown files. + +For example, if you have the following directory structure, you would set `backstage.io/adr-location: docs/adrs`: + +``` +repo-root/ + README.md + src/ + catalog-info.yaml + docs/ + adrs/ + 0001-use-adrs.md + 0002-use-cloud.md +``` + +### Search + +First, make sure to setup Backstage Search with the [ADR backend plugin](../adr-backend/README.md). +Afterwards, add the following code snippet to use `AdrSearchResultListItem` when the type of the search results is `adr`: + +```tsx +// In packages/app/src/components/search/SearchPage.tsx +import { AdrSearchResultListItem } from '@backstage/plugin-adr'; + +... + +case 'adr': + return ( + + ); +``` + +## Custom ADR formats + +By default, this plugin will parse ADRs according to the format specified by the [Markdown Architecture Decision Record (MADR)](https://adr.github.io/madr/) template. If your ADRs are written using a different format, you can apply the following customizations to correctly identify and parse your documents: + +### Custom Filename/Path Format + +In order to ensure the plugin fetches the correct ADR files (e.g. ignoring your template file), you can pass in an optional `filePathFilterFn` parameter to `EntityAdrContent` which will be called with each file path relative to the ADR location specified by `backstage.io/adr-location`. For example, the follow custom filter function will ignore the ADR template file and include files with a specific naming convention including those under a specified sub-directory: + +```tsx +const myCustomFilterFn: AdrFilePathFilterFn = (path: string): boolean => { + if (path === '0000-adr-template.md') { + return false; + } + // Match all files following the pattern NNNN-title-with-dashes.md including those under decided-adrs/ + return /^(decided-adrs\/)?\d{4}-.+\.md$/.test(path); +} + +... + + +``` + +### Custom Content Decorators + +Your ADR Markdown content will typically be rendered in the UI as is with the exception of relative links/embeds being rewritten as absolute URLs so they can be linked correctly (e.g. `./my-diagram.png` => `/my-diagram.png`). Depending on your ADR format, you may want to apply additional transformations to the content (e.g. parsing/ignoring front matter). You can do so by passing in a list of custom content decorators for the optional `contentDecorators` parameter. Note that passing in this parameter will override the default decorators. If you want to include the default ones, make sure to add them as well: + +```tsx +import { + AdrReader, + ... +} from '@backstage/plugin-adr'; + +... + +const myCustomDecorator: AdrContentDecorator = ({ content }) => { + return { content: applyCustomContentTransformation(content) }; +}; + +... + + +``` diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md new file mode 100644 index 0000000000..7d49689fc2 --- /dev/null +++ b/plugins/adr/api-report.md @@ -0,0 +1,64 @@ +## API Report File for "@backstage/plugin-adr" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { AdrDocument } from '@backstage/plugin-adr-common'; +import { AdrFilePathFilterFn } from '@backstage/plugin-adr-common'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public +export type AdrContentDecorator = (adrInfo: { + baseUrl: string; + content: string; +}) => { + content: string; +}; + +// @public +export const adrPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// @public +export const AdrReader: { + ({ + adr, + decorators, + }: { + adr: string; + decorators?: AdrContentDecorator[] | undefined; + }): JSX.Element; + decorators: Readonly<{ + createRewriteRelativeLinksDecorator(): AdrContentDecorator; + createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; + }>; +}; + +// @public +export const AdrSearchResultListItem: ({ + lineClamp, + result, +}: { + lineClamp?: number | undefined; + result: AdrDocument; +}) => JSX.Element; + +// @public +export const EntityAdrContent: ({ + contentDecorators, + filePathFilterFn, +}: { + contentDecorators?: AdrContentDecorator[] | undefined; + filePathFilterFn?: AdrFilePathFilterFn | undefined; +}) => JSX.Element; + +export { isAdrAvailable }; +``` diff --git a/plugins/adr/package.json b/plugins/adr/package.json new file mode 100644 index 0000000000..0f3b220ce5 --- /dev/null +++ b/plugins/adr/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-adr", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-adr-common": "^0.0.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "git-url-parse": "^11.6.0", + "octokit": "^1.7.1", + "react-markdown": "^8.0.0", + "react-router-dom": "6.0.0-beta.0", + "react-text-truncate": "^0.18.0", + "react-use": "^17.2.4", + "remark-gfm": "^3.0.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/git-url-parse": "^9.0.0", + "@types/jest": "*", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx new file mode 100644 index 0000000000..ae7dc6c271 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo } from 'react'; +import { + InfoCard, + MarkdownContent, + Progress, + WarningPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { getAdrLocationUrl } from '@backstage/plugin-adr-common'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { useOctokitRequest } from '../../hooks'; +import { adrDecoratorFactories } from './decorators'; +import { AdrContentDecorator } from './types'; + +/** + * Component to fetch and render an ADR. + * @public + */ +export const AdrReader = ({ + adr, + decorators, +}: { + adr: string; + decorators?: AdrContentDecorator[]; +}) => { + const { entity } = useEntity(); + const scmIntegrations = useApi(scmIntegrationsApiRef); + const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); + + const { value, loading, error } = useOctokitRequest( + `${adrLocationUrl}/${adr}`, + ); + + const adrContent = useMemo(() => { + if (!value?.data) { + return ''; + } + const adrDecorators = decorators ?? [ + adrDecoratorFactories.createRewriteRelativeLinksDecorator(), + adrDecoratorFactories.createRewriteRelativeEmbedsDecorator(), + ]; + + return adrDecorators.reduce( + (content, decorator) => + decorator({ baseUrl: adrLocationUrl, content }).content, + value.data, + ); + }, [adrLocationUrl, decorators, value]); + + return ( + + {loading && } + + {!loading && error && ( + + )} + + {!loading && !error && value?.data && ( + + )} + + ); +}; + +AdrReader.decorators = adrDecoratorFactories; diff --git a/plugins/adr/src/components/AdrReader/decorators.ts b/plugins/adr/src/components/AdrReader/decorators.ts new file mode 100644 index 0000000000..f24bb48a00 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/decorators.ts @@ -0,0 +1,47 @@ +/* + * 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 { AdrContentDecorator } from './types'; + +/** + * + * Factory for creating default ADR content decorators. The adrDecoratorFactories + * symbol is not directly exported, but through the AdrReader.decorators field. + * @public + */ +export const adrDecoratorFactories = Object.freeze({ + /** + * Rewrites relative Markdown links as absolute links. + */ + createRewriteRelativeLinksDecorator(): AdrContentDecorator { + return ({ baseUrl, content }) => ({ + content: content.replace( + /\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.md)\)/gim, + `[$1](${baseUrl}/$2$3)`, + ), + }); + }, + /** + * Rewrites relative Markdown embeds using absolute URLs. + */ + createRewriteRelativeEmbedsDecorator(): AdrContentDecorator { + return ({ baseUrl, content }) => ({ + content: content.replace( + /!\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/gim, + `![$1](${baseUrl}/$2$3$4)`, + ), + }); + }, +}); diff --git a/plugins/adr/src/components/AdrReader/index.ts b/plugins/adr/src/components/AdrReader/index.ts new file mode 100644 index 0000000000..8482351016 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './AdrReader'; +export * from './types'; diff --git a/plugins/adr/src/components/AdrReader/types.ts b/plugins/adr/src/components/AdrReader/types.ts new file mode 100644 index 0000000000..f12906c00b --- /dev/null +++ b/plugins/adr/src/components/AdrReader/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * ADR content decorator function type. Decorators are responsible for + * performing any necessary transformations on the ADR content before rendering. + * @public + */ +export type AdrContentDecorator = (adrInfo: { + baseUrl: string; + content: string; +}) => { content: string }; diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx new file mode 100644 index 0000000000..bd8e4be3cf --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { + Content, + ContentHeader, + MissingAnnotationEmptyState, + Progress, + SupportButton, + WarningPanel, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { + AdrFilePathFilterFn, + ANNOTATION_ADR_LOCATION, + getAdrLocationUrl, + isAdrAvailable, + madrFilePathFilter, +} from '@backstage/plugin-adr-common'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + Grid, + List, + ListItem, + ListItemText, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; + +import { useOctokitRequest } from '../../hooks'; +import { rootRouteRef } from '../../routes'; +import { AdrContentDecorator, AdrReader } from '../AdrReader'; + +const useStyles = makeStyles((theme: Theme) => ({ + adrMenu: { + backgroundColor: theme.palette.background.paper, + }, +})); + +/** + * Component for browsing ADRs on an entity page. + * @public + */ +export const EntityAdrContent = ({ + contentDecorators, + filePathFilterFn, +}: { + contentDecorators?: AdrContentDecorator[]; + filePathFilterFn?: AdrFilePathFilterFn; +}) => { + const classes = useStyles(); + const { entity } = useEntity(); + const rootLink = useRouteRef(rootRouteRef); + const [adrList, setAdrList] = useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + const scmIntegrations = useApi(scmIntegrationsApiRef); + const entityHasAdrs = isAdrAvailable(entity); + + const { value, loading, error } = useOctokitRequest( + getAdrLocationUrl(entity, scmIntegrations), + ); + + const selectedAdr = + adrList.find(adr => adr === searchParams.get('record')) ?? ''; + useEffect(() => { + if (adrList.length && !selectedAdr) { + searchParams.set('record', adrList[0]); + setSearchParams(searchParams, { replace: true }); + } + }); + + useEffect(() => { + if (!value?.data) { + return; + } + + const adrs = value.data + .filter( + (item: { type: string; name: string }) => + item.type === 'file' && + (filePathFilterFn + ? filePathFilterFn(item.name) + : madrFilePathFilter(item.name)), + ) + .map(({ name }: { name: string }) => name); + + setAdrList(adrs); + }, [filePathFilterFn, value]); + + return ( + + + + + + {!entityHasAdrs && ( + + )} + + {loading && } + + {entityHasAdrs && !loading && error && ( + + )} + + {entityHasAdrs && + !loading && + !error && + (adrList.length ? ( + + + + {adrList.map((adr, idx) => ( + + + + ))} + + + + + + + ) : ( + No ADRs found + ))} + + ); +}; diff --git a/plugins/adr/src/components/EntityAdrContent/index.ts b/plugins/adr/src/components/EntityAdrContent/index.ts new file mode 100644 index 0000000000..4552d4e87d --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityAdrContent } from './EntityAdrContent'; diff --git a/plugins/adr/src/hooks/index.ts b/plugins/adr/src/hooks/index.ts new file mode 100644 index 0000000000..d5aa102fb8 --- /dev/null +++ b/plugins/adr/src/hooks/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './useOctokitRequest'; diff --git a/plugins/adr/src/hooks/useOctokitRequest.ts b/plugins/adr/src/hooks/useOctokitRequest.ts new file mode 100644 index 0000000000..f160651169 --- /dev/null +++ b/plugins/adr/src/hooks/useOctokitRequest.ts @@ -0,0 +1,59 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import useAsync from 'react-use/lib/useAsync'; +import { Octokit } from 'octokit'; +import { useApi } from '@backstage/core-plugin-api'; +import { + scmAuthApiRef, + scmIntegrationsApiRef, +} from '@backstage/integration-react'; + +/** + * Hook for triggering authenticated Octokit requests against the GitHub Content API + * @public + */ +export const useOctokitRequest = (request: string): any => { + const authApi = useApi(scmAuthApiRef); + const scmIntegrations = useApi(scmIntegrationsApiRef); + + const { owner, name, ref, filepath } = parseGitUrl(request); + const path = filepath.replace(/^\//, ''); + const baseUrl = scmIntegrations.github.byUrl(request)?.config.apiBaseUrl; + + return useAsync(async () => { + const { token } = await authApi.getCredentials({ + url: request, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); + const octokit = new Octokit({ + auth: token, + baseUrl, + }); + + return octokit.request( + `GET /repos/${owner}/${name}/contents/${path}?ref=${ref}`, + { + headers: { Accept: 'application/vnd.github.v3.raw' }, + }, + ); + }, [request]); +}; diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts new file mode 100644 index 0000000000..84012ccb7a --- /dev/null +++ b/plugins/adr/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +/** + * ADR frontend plugin + * + * @packageDocumentation + */ +export { isAdrAvailable } from '@backstage/plugin-adr-common'; +export * from './components/AdrReader'; +export { adrPlugin, EntityAdrContent } from './plugin'; +export * from './search'; diff --git a/plugins/adr/src/plugin.test.ts b/plugins/adr/src/plugin.test.ts new file mode 100644 index 0000000000..e311bf98e6 --- /dev/null +++ b/plugins/adr/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { adrPlugin } from './plugin'; + +describe('adr', () => { + it('should export plugin', () => { + expect(adrPlugin).toBeDefined(); + }); +}); diff --git a/plugins/adr/src/plugin.ts b/plugins/adr/src/plugin.ts new file mode 100644 index 0000000000..03437b1f0b --- /dev/null +++ b/plugins/adr/src/plugin.ts @@ -0,0 +1,46 @@ +/* + * 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 { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** + * The Backstage plugin that holds ADR specific components + * @public + */ +export const adrPlugin = createPlugin({ + id: 'adr', + routes: { + root: rootRouteRef, + }, +}); + +/** + * An extension for browsing ADRs on an entity page. + * @public + */ +export const EntityAdrContent = adrPlugin.provide( + createRoutableExtension({ + name: 'EntityAdrContent', + component: () => + import('./components/EntityAdrContent').then(m => m.EntityAdrContent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/adr/src/routes.ts b/plugins/adr/src/routes.ts new file mode 100644 index 0000000000..0ec5c71294 --- /dev/null +++ b/plugins/adr/src/routes.ts @@ -0,0 +1,24 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +/** + * Root route ref for the ADR plugin + * @public + */ +export const rootRouteRef = createRouteRef({ + id: 'adr', +}); diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx new file mode 100644 index 0000000000..8b89629af6 --- /dev/null +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import TextTruncate from 'react-text-truncate'; +import { + Box, + Chip, + Divider, + ListItem, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import { Link } from '@backstage/core-components'; +import { AdrDocument } from '@backstage/plugin-adr-common'; + +const useStyles = makeStyles({ + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, +}); + +/** + * A component to display a ADR search result + * @public + */ +export const AdrSearchResultListItem = ({ + lineClamp = 5, + result, +}: { + lineClamp?: number; + result: AdrDocument; +}) => { + const classes = useStyles(); + + return ( + + + + } + /> + + {result.status && ( + + )} + {result.date && } + + + + + ); +}; diff --git a/plugins/adr/src/search/index.ts b/plugins/adr/src/search/index.ts new file mode 100644 index 0000000000..519d8cb91f --- /dev/null +++ b/plugins/adr/src/search/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './AdrSearchResultListItem'; diff --git a/plugins/adr/src/setupTests.ts b/plugins/adr/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/adr/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 9514e64539..667123afb9 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-airbrake-backend +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 021db97be8..28ff3bd6a7 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.4-next.0", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@types/express": "*", "express": "^4.17.1", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 516dfd9378..dcd549b93a 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-airbrake +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/test-utils@1.0.2-next.0 + - @backstage/dev-utils@1.0.2-next.0 + +## 0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/dev-utils@1.0.1 + - @backstage/test-utils@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/dev-utils@1.0.1-next.1 + - @backstage/test-utils@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 383597e7a3..87e42a5b85 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,12 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/app-defaults": "^1.0.1-next.1", - "@backstage/cli": "^0.17.0-next.2", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/app-defaults": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index b28d256c22..ad74422791 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-allure +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.20 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.20-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.20-next.0 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index a8c1745518..242d0ddd2d 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.20-next.0", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,10 +25,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index 4c6483b998..75c54bccf8 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-analytics-module-ga +## 0.1.15 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.15-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.1.15-next.0 ### Patch Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 42e4c7e3e5..f8c91e340e 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.15-next.0", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index d5c6475f42..b324e3e4f9 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-apache-airflow +## 0.1.12 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.12-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.1.12-next.0 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 86f249a89a..b4ed8c94da 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.1.12-next.0", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 2b95d883f0..11a9c99e74 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-api-docs +## 0.8.5-next.0 + +### Patch Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute +- 09fa05e5d7: Bumped @asyncapi/react-component > openapi-sampler > json-pointer to resolve security vulnerability CVE-2021-23820 +- 433a609a15: Updated the rendering of AsyncApi definitions to be able to resolve absolute HTTP \$ref references. +- Updated dependencies + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.8.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- ae321bd414: build(deps): bump `@asyncapi/react-component` +- 230ad0826f: Bump to using `@types/node` v16 +- 1c8ebf7af2: Add dedicated gRPC api definition widget +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog@1.1.0 + +## 0.8.4-next.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 1c8ebf7af2: Add dedicated gRPC api definition widget +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-catalog@1.1.0-next.3 + ## 0.8.4-next.2 ### Patch Changes diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index 51588aacf4..bea820cb61 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -156,7 +156,7 @@ The Swagger UI package by expects to have a route to `/oauth2-redirect.html` whi the redirect callback for the OAuth2 Authorization Code flow, however, this file is not installed by this plugin. -Grab a copy of [oauth2-redirect.html](https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html) +Grab a copy of [`oauth2-redirect.html`](https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html) and put it in the `app/public/` directory in order to enable Swagger UI to complete this redirection. This also may require you to adjust `Content Security Policy` header settings of your Backstage application, so that the script in `oauth2-redirect.html` can be executed. Since the script is static we can add the hash of it directly to our CSP policy, which we do by adding the following to the `csp` section of the app configuration: diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index b6683d9b40..991c73d4ab 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -10,6 +10,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { InfoCardVariants } from '@backstage/core-components'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; @@ -128,7 +129,7 @@ export const EntityApiDefinitionCard: () => JSX.Element; export const EntityConsumedApisCard: ({ variant, }: { - variant?: 'gridItem' | undefined; + variant?: InfoCardVariants | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityConsumingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -137,7 +138,7 @@ export const EntityConsumedApisCard: ({ export const EntityConsumingComponentsCard: ({ variant, }: { - variant?: 'gridItem' | undefined; + variant?: InfoCardVariants | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityHasApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -146,7 +147,7 @@ export const EntityConsumingComponentsCard: ({ export const EntityHasApisCard: ({ variant, }: { - variant?: 'gridItem' | undefined; + variant?: InfoCardVariants | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityProvidedApisCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -155,7 +156,7 @@ export const EntityHasApisCard: ({ export const EntityProvidedApisCard: ({ variant, }: { - variant?: 'gridItem' | undefined; + variant?: InfoCardVariants | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityProvidingComponentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -164,7 +165,7 @@ export const EntityProvidedApisCard: ({ export const EntityProvidingComponentsCard: ({ variant, }: { - variant?: 'gridItem' | undefined; + variant?: InfoCardVariants | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "GraphQlDefinitionWidget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 23ebd1cf8a..8556b13aee 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.4-next.2", + "version": "0.8.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,17 +33,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.34", - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog": "^1.1.0-next.2", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@asyncapi/react-component": "1.0.0-next.37", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "graphiql": "^1.5.12", + "graphiql": "^1.8.8", "graphql": "^16.0.0", "isomorphic-form-data": "^2.0.0", "react-router": "6.0.0-beta.0", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index 936edc07da..e2938dc9fc 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -23,6 +23,9 @@ import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { ApiDefinitionCard } from './ApiDefinitionCard'; +// Make sure this is in the require cache before the async rendering happens +import '../OpenApiDefinitionWidget/OpenApiDefinition'; + describe('', () => { const apiDocsConfig: jest.Mocked = { getApiDefinitionWidget: jest.fn(), diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx index e810e06ff7..fde41425c9 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -26,13 +26,14 @@ import { apiEntityColumns } from './presets'; import { CodeSnippet, InfoCard, + InfoCardVariants, Link, Progress, WarningPanel, } from '@backstage/core-components'; type Props = { - variant?: 'gridItem'; + variant?: InfoCardVariants; }; export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => { diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx index c3d79414c2..bb22f82269 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx @@ -26,6 +26,7 @@ import { createSpecApiTypeColumn } from './presets'; import { CodeSnippet, InfoCard, + InfoCardVariants, Link, Progress, TableColumn, @@ -33,7 +34,7 @@ import { } from '@backstage/core-components'; type Props = { - variant?: 'gridItem'; + variant?: InfoCardVariants; }; const columns: TableColumn[] = [ diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx index a63546176f..6ffb6365d2 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -26,13 +26,14 @@ import { apiEntityColumns } from './presets'; import { CodeSnippet, InfoCard, + InfoCardVariants, Link, Progress, WarningPanel, } from '@backstage/core-components'; type Props = { - variant?: 'gridItem'; + variant?: InfoCardVariants; }; export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => { diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx index 3f9375921d..fd8948828c 100644 --- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx +++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx @@ -142,6 +142,21 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ }, })); +const fetchResolver = { + order: 199, // Use 199 as the built-in http resolver is 200 + canRead: /^https?:\/\//, + async read(file: any) { + const response = await fetch(file.url); + return response.text(); + }, +}; + +const config = { + parserOptions: { + resolve: { fetch: fetchResolver }, + }, +}; + type Props = { definition: string; }; @@ -155,7 +170,7 @@ export const AsyncApiDefinition = ({ definition }: Props): JSX.Element => { return (
- +
); }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx index 5455f37f14..af3dcb7204 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -28,13 +28,14 @@ import React from 'react'; import { CodeSnippet, InfoCard, + InfoCardVariants, Link, Progress, WarningPanel, } from '@backstage/core-components'; type Props = { - variant?: 'gridItem'; + variant?: InfoCardVariants; }; export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => { diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx index 9bcace393e..0be746de6d 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -28,13 +28,14 @@ import React from 'react'; import { CodeSnippet, InfoCard, + InfoCardVariants, Link, Progress, WarningPanel, } from '@backstage/core-components'; type Props = { - variant?: 'gridItem'; + variant?: InfoCardVariants; }; export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => { diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index ff734beb44..9a0ab7aae7 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-app-backend +## 0.3.32-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/config-loader@1.1.1-next.0 + +## 0.3.31 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.1.0 + - @backstage/backend-common@0.13.2 + +## 0.3.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + - @backstage/config-loader@1.1.0-next.1 + ## 0.3.31-next.0 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index e7433aa9ae..88b4b43f1e 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.31-next.0", + "version": "0.3.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,15 +33,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/config-loader": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/config-loader": "^1.1.1-next.0", "@backstage/config": "^1.0.0", "@backstage/types": "^1.0.0", "@types/express": "^4.17.6", "globby": "^11.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "helmet": "^5.0.2", "knex": "^1.0.2", "lodash": "^4.17.21", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@backstage/types": "^1.0.0", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 96ddb6afe8..218671f378 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,300 @@ # @backstage/plugin-auth-backend +## 0.13.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 787ae0d541: Add more common predefined sign-in resolvers to auth providers. + + Add the existing resolver to more providers (already available at `google`): + + - `providers.microsoft.resolvers.emailLocalPartMatchingUserEntityName()` + - `providers.okta.resolvers.emailLocalPartMatchingUserEntityName()` + + Add a new resolver for simple email-to-email matching: + + - `providers.google.resolvers.emailMatchingUserEntityProfileEmail()` + - `providers.microsoft.resolvers.emailMatchingUserEntityProfileEmail()` + - `providers.okta.resolvers.emailMatchingUserEntityProfileEmail()` + +- 9ec4e0613e: Update to `jose` 4.6.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## 0.13.0 + +### Minor Changes + +- 15d3a3c39a: **BREAKING**: All sign-in resolvers must now return a `token` in their sign-in result. Returning an `id` is no longer supported. +- c5aeaf339d: **BREAKING**: All auth providers have had their default sign-in resolvers removed. This means that if you want to use a particular provider for sign-in, you must provide an explicit sign-in resolver. For more information on how to configure sign-in resolvers, see the [sign-in resolver documentation](https://backstage.io/docs/auth/identity-resolver). + +### Patch Changes + +- c5aeaf339d: **DEPRECATION**: The `AuthProviderFactoryOptions` type has been deprecated, as the options are now instead inlined in the `AuthProviderFactory` type. This will make it possible to more easily introduce new options in the future without a possibly breaking change. +- 794f7542b6: Updated openid-client from 4.1.2 to 5.1.3 +- c5aeaf339d: **DEPRECATION**: The `getEntityClaims` helper has been deprecated, with `getDefaultOwnershipEntityRefs` being added to replace it. +- de231e5b06: Declare oauth2 `clientSecret` with visibility secret +- c5aeaf339d: **DEPRECATION**: All `createProvider` and `*SignInResolver` have been deprecated. Instead, a single `providers` object is exported which contains all built-in auth providers. + + If you have a setup that currently looks for example like this: + + ```ts + import { + createRouter, + defaultAuthProviderFactories, + createGoogleProvider, + googleEmailSignInResolver, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: createGoogleProvider({ + signIn: { + resolver: googleEmailSignInResolver, + }, + }), + }, + }); + } + ``` + + You would migrate it to something like this: + + ```ts + import { + createRouter, + providers, + defaultAuthProviderFactories, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: providers.google.create({ + signIn: { + resolver: + providers.google.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); + } + ``` + +- 2cc1d1b235: Applied the fix from version 0.12.3 of this package, which is part of the v1.0.1 release of Backstage. +- c5aeaf339d: **DEPRECATION** The `AuthResolverContext` has received a number of changes, which is the context used by auth handlers and sign-in resolvers. + + The following fields deprecated: `logger`, `tokenIssuer`, `catalogIdentityClient`. If you need to access the `logger`, you can do so through a closure instead. The `tokenIssuer` has been replaced with an `issueToken` method, which is available directory on the context. The `catalogIdentityClient` has been replaced by the `signInWithCatalogUser` method, as well as the lower level `findCatalogUser` method and `getDefaultOwnershipEntityRefs` helper. + + It should be possible to migrate most sign-in resolvers to more or less only use `signInWithCatalogUser`, for example an email lookup resolver like this one: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + ``` + + can be migrated to the following: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + }; + ``` + + While a direct entity name lookup using a user ID might look like this: + + ```ts + async ({ result: { fullProfile } }, ctx) => { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.userId, + }, + }); + }; + ``` + + If you want more control over the way that users are looked up, ownership is assigned, or tokens are issued, you can use a combination of the `findCatalogUser`, `getDefaultOwnershipEntityRefs`, and `issueToken` instead. + +- f4cdf4cac1: Defensively encode URL parameters when fetching ELB keys +- 6ee04078e1: **DEPRECATION**: The `tokenIssuer` option for `OAuthAdapter` is no longer needed and has been deprecated. +- a45bce06e3: Handle trailing slashes on GitHub `enterpriseInstanceUrl` settings +- 45f7a261c7: Bumped passport-microsoft to resolve CVE-2021-41580 +- c5aeaf339d: Added exports of the following types: `AuthProviderConfig`, `StateEncoder`, `TokenParams`, `AwsAlbResult`. +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + +## 0.13.0-next.2 + +### Minor Changes + +- c5aeaf339d: **BREAKING**: All auth providers have had their default sign-in resolvers removed. This means that if you want to use a particular provider for sign-in, you must provide an explicit sign-in resolver. For more information on how to configure sign-in resolvers, see the [sign-in resolver documentation](https://backstage.io/docs/auth/identity-resolver). + +### Patch Changes + +- c5aeaf339d: **DEPRECATION**: The `AuthProviderFactoryOptions` type has been deprecated, as the options are now instead inlined in the `AuthProviderFactory` type. This will make it possible to more easily introduce new options in the future without a possibly breaking change. +- 794f7542b6: Updated openid-client from 4.1.2 to 5.1.3 +- c5aeaf339d: **DEPRECATION**: The `getEntityClaims` helper has been deprecated, with `getDefaultOwnershipEntityRefs` being added to replace it. +- de231e5b06: Declare oauth2 `clientSecret` with visibility secret +- c5aeaf339d: **DEPRECATION**: All `createProvider` and `*SignInResolver` have been deprecated. Instead, a single `providers` object is exported which contains all built-in auth providers. + + If you have a setup that currently looks for example like this: + + ```ts + import { + createRouter, + defaultAuthProviderFactories, + createGoogleProvider, + googleEmailSignInResolver, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: createGoogleProvider({ + signIn: { + resolver: googleEmailSignInResolver, + }, + }), + }, + }); + } + ``` + + You would migrate it to something like this: + + ```ts + import { + createRouter, + providers, + defaultAuthProviderFactories, + } from '@backstage/plugin-auth-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + ...env, + providerFactories: { + ...defaultAuthProviderFactories, + google: providers.google.create({ + signIn: { + resolver: + providers.google.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); + } + ``` + +- c5aeaf339d: **DEPRECATION** The `AuthResolverContext` has received a number of changes, which is the context used by auth handlers and sign-in resolvers. + + The following fields deprecated: `logger`, `tokenIssuer`, `catalogIdentityClient`. If you need to access the `logger`, you can do so through a closure instead. The `tokenIssuer` has been replaced with an `issueToken` method, which is available directory on the context. The `catalogIdentityClient` has been replaced by the `signInWithCatalogUser` method, as well as the lower level `findCatalogUser` method and `getDefaultOwnershipEntityRefs` helper. + + It should be possible to migrate most sign-in resolvers to more or less only use `signInWithCatalogUser`, for example an email lookup resolver like this one: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; + }; + ``` + + can be migrated to the following: + + ```ts + async ({ profile }, ctx) => { + if (!profile.email) { + throw new Error('Profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'acme.org/email': profile.email, + }, + }); + }; + ``` + + While a direct entity name lookup using a user ID might look like this: + + ```ts + async ({ result: { fullProfile } }, ctx) => { + return ctx.signInWithCatalogUser({ + entityRef: { + name: fullProfile.userId, + }, + }); + }; + ``` + + If you want more control over the way that users are looked up, ownership is assigned, or tokens are issued, you can use a combination of the `findCatalogUser`, `getDefaultOwnershipEntityRefs`, and `issueToken` instead. + +- f4cdf4cac1: Defensively encode URL parameters when fetching ELB keys +- c5aeaf339d: Added exports of the following types: `AuthProviderConfig`, `StateEncoder`, `TokenParams`, `AwsAlbResult`. +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + ## 0.13.0-next.1 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 4227f1db0a..18924f7c23 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -9,7 +9,9 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -42,9 +44,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// Warning: (ae-missing-release-tag) "AtlassianProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type AtlassianProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -52,7 +52,7 @@ export type AtlassianProviderOptions = { }; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type Auth0ProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -71,16 +71,32 @@ export type AuthHandlerResult = { profile: ProfileInfo; }; +// @public (undocumented) +export type AuthProviderConfig = { + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}; + // Warning: (ae-missing-release-tag) "AuthProviderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type AuthProviderFactory = ( - options: AuthProviderFactoryOptions, -) => AuthProviderRouteHandlers; +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + resolverContext: AuthResolverContext; + tokenManager: TokenManager; + tokenIssuer: TokenIssuer; + discovery: PluginEndpointDiscovery; + catalogApi: CatalogApi; +}) => AuthProviderRouteHandlers; // Warning: (ae-missing-release-tag) "AuthProviderFactoryOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -102,15 +118,40 @@ export interface AuthProviderRouteHandlers { start(req: express.Request, res: express.Response): Promise; } +// @public +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: Exclude; + }; + // @public export type AuthResolverContext = { + logger: Logger; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + issueToken(params: TokenParams): Promise<{ + token: string; + }>; + findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ + entity: Entity; + }>; + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; }; -// Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AuthResponse = { providerInfo: ProviderInfo; @@ -118,9 +159,7 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; -// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type AwsAlbProviderOptions = { authHandler?: AuthHandler; signIn: { @@ -128,6 +167,13 @@ export type AwsAlbProviderOptions = { }; }; +// @public (undocumented) +export type AwsAlbResult = { + fullProfile: Profile; + expiresInSeconds?: number; + accessToken: string; +}; + // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -159,9 +205,7 @@ export type BitbucketPassportProfile = Profile & { }; }; -// Warning: (ae-missing-release-tag) "BitbucketProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -169,15 +213,11 @@ export type BitbucketProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "bitbucketUserIdSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketUserIdSignInResolver: SignInResolver; +// @public @deprecated (undocumented) +export const bitbucketUserIdSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "bitbucketUsernameSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const bitbucketUsernameSignInResolver: SignInResolver; +// @public @deprecated (undocumented) +export const bitbucketUsernameSignInResolver: SignInResolver; // Warning: (ae-missing-release-tag) "CatalogIdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -201,94 +241,187 @@ export type CookieConfigurer = (ctx: { secure: boolean; }; -// Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createAtlassianProvider: ( - options?: AtlassianProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public (undocumented) +// @public @deprecated (undocumented) export const createAuth0Provider: ( - options?: Auth0ProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createAwsAlbProvider: ( - options?: AwsAlbProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createBitbucketProvider: ( - options?: BitbucketProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public -export function createGcpIapProvider( - options: GcpIapProviderOptions, -): AuthProviderFactory; +// @public @deprecated (undocumented) +export const createGcpIapProvider: (options: { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; +}) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGithubProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createGithubProvider: ( - options?: GithubProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + stateEncoder?: StateEncoder | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGitlabProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createGitlabProvider: ( - options?: GitlabProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createGoogleProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createGoogleProvider: ( - options?: GoogleProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createMicrosoftProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createMicrosoftProvider: ( - options?: MicrosoftProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOAuth2Provider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOAuth2Provider: ( - options?: OAuth2ProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public -export const createOauth2ProxyProvider: ( - options: Oauth2ProxyProviderOptions, -) => AuthProviderFactory; +// @public @deprecated (undocumented) +export const createOauth2ProxyProvider: (options: { + authHandler: AuthHandler>; + signIn: { + resolver: SignInResolver>; + }; +}) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOidcProvider: ( - options?: OidcProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "createOktaProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const createOktaProvider: ( - _options?: OktaProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; -// @public (undocumented) +// @public @deprecated (undocumented) export const createOneLoginProvider: ( - options?: OneLoginProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -301,9 +434,18 @@ export function createOriginFilter(config: Config): (origin: string) => boolean; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export const createSamlProvider: ( - options?: SamlProviderOptions | undefined, + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, ) => AuthProviderFactory; // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -323,7 +465,7 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public +// @public @deprecated (undocumented) export type GcpIapProviderOptions = { authHandler?: AuthHandler; signIn: { @@ -343,10 +485,12 @@ export type GcpIapTokenInfo = { [key: string]: JsonValue; }; -// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts +// @public +export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; + // Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export function getEntityClaims(entity: UserEntity): TokenParams['claims']; // Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -363,68 +507,54 @@ export type GithubOAuthResult = { refreshToken?: string; }; -// Warning: (ae-missing-release-tag) "GithubProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type GithubProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; stateEncoder?: StateEncoder; }; -// Warning: (ae-missing-release-tag) "GitlabProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type GitlabProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// Warning: (ae-missing-release-tag) "googleEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const googleEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "GoogleProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type GoogleProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// Warning: (ae-missing-release-tag) "microsoftEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const microsoftEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "MicrosoftProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type MicrosoftProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// Warning: (ae-missing-release-tag) "OAuth2ProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuth2ProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// @public +// @public @deprecated (undocumented) export type Oauth2ProxyProviderOptions = { authHandler: AuthHandler>; signIn: { @@ -574,30 +704,26 @@ export type OidcAuthResult = { userinfo: UserinfoResponse; }; -// @public +// @public @deprecated (undocumented) export type OidcProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const oktaEmailSignInResolver: SignInResolver; -// Warning: (ae-missing-release-tag) "OktaProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export type OktaProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OneLoginProviderOptions = { authHandler?: AuthHandler; signIn?: { @@ -626,6 +752,241 @@ export type ProfileInfo = { picture?: string; }; +// @public +export const providers: Readonly<{ + atlassian: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + auth0: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + awsAlb: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + bitbucket: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + usernameMatchingUserEntityAnnotation(): SignInResolver; + userIdMatchingUserEntityAnnotation(): SignInResolver; + }>; + }>; + gcpIap: Readonly<{ + create: (options: { + authHandler?: AuthHandler | undefined; + signIn: { + resolver: SignInResolver; + }; + }) => AuthProviderFactory; + resolvers: never; + }>; + github: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + stateEncoder?: StateEncoder | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + usernameMatchingUserEntityName: () => SignInResolver; + }>; + }>; + gitlab: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + google: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; + }>; + microsoft: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; + }>; + oauth2: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + oauth2Proxy: Readonly<{ + create: (options: { + authHandler: AuthHandler>; + signIn: { + resolver: SignInResolver>; + }; + }) => AuthProviderFactory; + resolvers: never; + }>; + oidc: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + okta: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailLocalPartMatchingUserEntityName: () => SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver; + emailMatchingUserEntityAnnotation(): SignInResolver; + }>; + }>; + onelogin: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: never; + }>; + saml: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + nameIdMatchingUserEntityName(): SignInResolver; + }>; + }>; +}>; + // Warning: (ae-missing-release-tag) "readState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -656,11 +1017,14 @@ export type SamlAuthResult = { fullProfile: any; }; -// @public (undocumented) +// @public @deprecated (undocumented) +export const samlNameIdEntityNameSignInResolver: SignInResolver; + +// @public @deprecated (undocumented) export type SamlProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; @@ -676,9 +1040,12 @@ export type SignInResolver = ( context: AuthResolverContext, ) => Promise; -// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @public (undocumented) +export type StateEncoder = (req: OAuthStartRequest) => Promise<{ + encodedState: string; +}>; + +// @public @deprecated export type TokenIssuer = { issueToken(params: TokenParams): Promise; listPublicKeys(): Promise<{ @@ -686,6 +1053,14 @@ export type TokenIssuer = { }>; }; +// @public +export type TokenParams = { + claims: { + sub: string; + ent?: string[]; + }; +}; + // Warning: (ae-missing-release-tag) "verifyNonce" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -706,8 +1081,5 @@ export type WebMessageResponse = // Warnings were encountered during analysis: // -// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:118:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/identity/types.d.ts:38:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 756519e490..834802379b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.13.0-next.1", + "version": "0.13.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@backstage/types": "^1.0.0", @@ -49,9 +49,9 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "google-auth-library": "^7.6.1", - "jose": "^1.27.1", + "jose": "^4.6.0", "jwt-decode": "^3.1.0", "knex": "^1.0.2", "lodash": "^4.17.21", @@ -66,7 +66,7 @@ "passport-github2": "^0.1.12", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", - "passport-microsoft": "^0.1.0", + "passport-microsoft": "^1.0.0", "passport-oauth2": "^1.6.1", "passport-okta-oauth": "^0.0.1", "passport-onelogin-oauth": "^0.0.1", @@ -76,8 +76,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index 4b1c1804df..3ae6b895bc 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -17,15 +17,16 @@ import { MemoryKeyStore } from './MemoryKeyStore'; import { TokenFactory } from './TokenFactory'; import { getVoidLogger } from '@backstage/backend-common'; -import { JWKS, JSONWebKey, JWT } from 'jose'; +import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from 'jose'; import { stringifyEntityRef } from '@backstage/catalog-model'; const logger = getVoidLogger(); function jwtKid(jwt: string): string { - const { header } = JWT.decode(jwt, { complete: true }) as { - header: { kid: string }; - }; + const header = decodeProtectedHeader(jwt); + if (!header.kid) { + throw new Error('JWT Header did not contain a key ID (kid)'); + } return header.kid; } @@ -46,25 +47,25 @@ describe('TokenFactory', () => { }); await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] }); - const token = await factory.issueToken({ claims: { sub: entityRef } }); - - const { keys } = await factory.listPublicKeys(); - const keyStore = JWKS.asKeyStore({ - keys: keys.map(key => key as JSONWebKey), + const token = await factory.issueToken({ + claims: { sub: entityRef, ent: [entityRef] }, }); - const payload = JWT.verify(token, keyStore) as object & { - iat: number; - exp: number; - }; - expect(payload).toEqual({ + const { keys } = await factory.listPublicKeys(); + const keyStore = createLocalJWKSet({ keys: keys }); + + const verifyResult = await jwtVerify(token, keyStore); + expect(verifyResult.payload).toEqual({ iss: 'my-issuer', aud: 'backstage', sub: entityRef, + ent: [entityRef], iat: expect.any(Number), exp: expect.any(Number), }); - expect(payload.exp).toBe(payload.iat + keyDurationSeconds); + expect(verifyResult.payload.exp).toBe( + verifyResult.payload.iat! + keyDurationSeconds, + ); }); it('should generate new signing keys when the current one expires', async () => { diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index cb2a8b453d..cb24d7ccd1 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -15,11 +15,12 @@ */ import { TokenIssuer, TokenParams, KeyStore, AnyJWK } from './types'; -import { JSONWebKey, JWK, JWS } from 'jose'; +import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; import { DateTime } from 'luxon'; import { parseEntityRef } from '@backstage/catalog-model'; +import { AuthenticationError } from '@backstage/errors'; const MS_IN_S = 1000; @@ -54,7 +55,7 @@ export class TokenFactory implements TokenIssuer { private readonly keyDurationSeconds: number; private keyExpiry?: Date; - private privateKeyPromise?: Promise; + private privateKeyPromise?: Promise; constructor(options: Options) { this.issuer = options.issuer; @@ -84,10 +85,18 @@ export class TokenFactory implements TokenIssuer { this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`); - return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { - alg: key.alg, - kid: key.kid, - }); + if (!key.alg) { + throw new AuthenticationError('No algorithm was provided in the key'); + } + + return new SignJWT({ iss, sub, ent, aud, iat, exp }) + .setProtectedHeader({ alg: key.alg, kid: key.kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(await importJWK(key)); } // This will be called by other services that want to verify ID tokens. @@ -127,7 +136,7 @@ export class TokenFactory implements TokenIssuer { return { keys: validKeys.map(({ key }) => key) }; } - private async getKey(): Promise { + private async getKey(): Promise { // Make sure that we only generate one key at a time if (this.privateKeyPromise) { if ( @@ -147,11 +156,11 @@ export class TokenFactory implements TokenIssuer { .toJSDate(); const promise = (async () => { // This generates a new signing key to be used to sign tokens until the next key rotation - const key = await JWK.generate('EC', 'P-256', { - use: 'sig', - kid: uuid(), - alg: 'ES256', - }); + const key = await generateKeyPair('ES256'); + const publicKey = await exportJWK(key.publicKey); + const privateKey = await exportJWK(key.privateKey); + publicKey.kid = privateKey.kid = uuid(); + publicKey.alg = privateKey.alg = 'ES256'; // We're not allowed to use the key until it has been successfully stored // TODO: some token verification implementations aggressively cache the list of keys, and @@ -159,11 +168,11 @@ export class TokenFactory implements TokenIssuer { // may want to keep using the existing key for some period of time until we switch to // the new one. This also needs to be implemented cross-service though, meaning new services // that boot up need to be able to grab an existing key to use for signing. - this.logger.info(`Created new signing key ${key.kid}`); - await this.keyStore.addKey(key.toJWK(false) as unknown as AnyJWK); + this.logger.info(`Created new signing key ${publicKey.kid}`); + await this.keyStore.addKey(publicKey as AnyJWK); // At this point we are allowed to start using the new key - return key as JSONWebKey; + return privateKey; })(); this.privateKeyPromise = promise; diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index faafd2b68e..5a350284e5 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -22,7 +22,11 @@ export interface AnyJWK extends Record { kty: string; } -/** Parameters used to issue new ID Tokens */ +/** + * Parameters used to issue new ID Tokens + * + * @public + */ export type TokenParams = { /** The claims that will be embedded within the token */ claims: { @@ -33,8 +37,12 @@ export type TokenParams = { }; }; +// TODO(Rugvip): This should at least be made internal /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. + * + * @public + * @deprecated This interface is deprecated and will be removed in a future release. */ export type TokenIssuer = { /** diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index d0cade087e..d51cb3f1eb 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,7 @@ */ export * from './service/router'; -export type { TokenIssuer } from './identity'; +export type { TokenIssuer, TokenParams } from './identity'; export * from './providers'; // flow package provides 2 functions @@ -32,3 +32,5 @@ export * from './lib/flow'; export * from './lib/oauth'; export * from './lib/catalog'; + +export { getDefaultOwnershipEntityRefs } from './lib/resolvers'; diff --git a/plugins/auth-backend/src/lib/catalog/helpers.ts b/plugins/auth-backend/src/lib/catalog/helpers.ts index db9b38e2a6..3ff1b5da05 100644 --- a/plugins/auth-backend/src/lib/catalog/helpers.ts +++ b/plugins/auth-backend/src/lib/catalog/helpers.ts @@ -21,6 +21,9 @@ import { } from '@backstage/catalog-model'; import { TokenParams } from '../../identity'; +/** + * @deprecated use {@link getDefaultOwnershipEntityRefs} instead + */ export function getEntityClaims(entity: UserEntity): TokenParams['claims'] { const userRef = stringifyEntityRef(entity); diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts new file mode 100644 index 0000000000..1a1fdd7a36 --- /dev/null +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -0,0 +1,143 @@ +/* + * 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 { TokenManager } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + DEFAULT_NAMESPACE, + Entity, + parseEntityRef, + RELATION_MEMBER_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../..'; +import { TokenParams } from '../../identity'; +import { AuthResolverContext } from '../../providers'; +import { AuthResolverCatalogUserQuery } from '../../providers/types'; +import { CatalogIdentityClient } from '../catalog'; + +/** + * Uses the default ownership resolution logic to return an array + * of entity refs that the provided entity claims ownership through. + * + * A reference to the entity itself will also be included in the returned array. + * + * @public + */ +export function getDefaultOwnershipEntityRefs(entity: Entity) { + const membershipRefs = + entity.relations + ?.filter( + r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'), + ) + .map(r => r.targetRef) ?? []; + + return Array.from(new Set([stringifyEntityRef(entity), ...membershipRefs])); +} + +/** + * @internal + */ +export class CatalogAuthResolverContext implements AuthResolverContext { + static create(options: { + logger: Logger; + catalogApi: CatalogApi; + tokenIssuer: TokenIssuer; + tokenManager: TokenManager; + }): CatalogAuthResolverContext { + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi: options.catalogApi, + tokenManager: options.tokenManager, + }); + return new CatalogAuthResolverContext( + options.logger, + options.tokenIssuer, + catalogIdentityClient, + options.catalogApi, + options.tokenManager, + ); + } + + private constructor( + public readonly logger: Logger, + public readonly tokenIssuer: TokenIssuer, + public readonly catalogIdentityClient: CatalogIdentityClient, + private readonly catalogApi: CatalogApi, + private readonly tokenManager: TokenManager, + ) {} + + async issueToken(params: TokenParams) { + const token = await this.tokenIssuer.issueToken(params); + return { token }; + } + + async findCatalogUser(query: AuthResolverCatalogUserQuery) { + let result: Entity[] | Entity | undefined = undefined; + const { token } = await this.tokenManager.getToken(); + + if ('entityRef' in query) { + const entityRef = parseEntityRef(query.entityRef, { + defaultKind: 'User', + defaultNamespace: DEFAULT_NAMESPACE, + }); + result = await this.catalogApi.getEntityByRef(entityRef, { token }); + } else if ('annotations' in query) { + const filter: Record = { + kind: 'user', + }; + for (const [key, value] of Object.entries(query.annotations)) { + filter[`metadata.annotations.${key}`] = value; + } + const res = await this.catalogApi.getEntities({ filter }, { token }); + result = res.items; + } else if ('filter' in query) { + const res = await this.catalogApi.getEntities( + { filter: query.filter }, + { token }, + ); + result = res.items; + } else { + throw new InputError('Invalid user lookup query'); + } + + if (Array.isArray(result)) { + if (result.length > 1) { + throw new ConflictError('User lookup resulted in multiple matches'); + } + result = result[0]; + } + if (!result) { + throw new NotFoundError('User not found'); + } + + return { entity: result }; + } + + async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) { + const { entity } = await this.findCatalogUser(query); + const ownershipRefs = getDefaultOwnershipEntityRefs(entity); + + const token = await this.tokenIssuer.issueToken({ + claims: { + sub: stringifyEntityRef(entity), + ent: ownershipRefs, + }, + }); + return { token }; + } +} diff --git a/plugins/auth-backend/src/lib/resolvers/index.ts b/plugins/auth-backend/src/lib/resolvers/index.ts new file mode 100644 index 0000000000..c1ca59cb25 --- /dev/null +++ b/plugins/auth-backend/src/lib/resolvers/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + CatalogAuthResolverContext, + getDefaultOwnershipEntityRefs, +} from './CatalogAuthResolverContext'; diff --git a/plugins/auth-backend/src/providers/atlassian/provider.test.ts b/plugins/auth-backend/src/providers/atlassian/provider.test.ts index 29241dd17b..c41a18aa22 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.test.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.test.ts @@ -16,11 +16,9 @@ import { AtlassianAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { OAuthResult } from '../../lib/oauth'; import { PassportProfile } from '../../lib/passport/types'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -28,19 +26,8 @@ const mockFrameHandler = jest.spyOn( ) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('createAtlassianProvider', () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new AtlassianAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 26eea420fc..2aa85dab96 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -37,22 +37,18 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; import express from 'express'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export const atlassianDefaultAuthHandler: AuthHandler = async ({ @@ -66,14 +62,10 @@ export class AtlassianAuthProvider implements OAuthHandlers { private readonly _strategy: AtlassianStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: AtlassianAuthProviderOptions) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; @@ -120,12 +112,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -143,7 +130,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -174,6 +161,10 @@ export class AtlassianAuthProvider implements OAuthHandlers { } } +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type AtlassianProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -189,51 +180,59 @@ export type AtlassianProviderOptions = { }; }; -export const createAtlassianProvider = ( - options?: AtlassianProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const scopes = envConfig.getString('scopes'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for atlassian auth + * + * @public + */ +export const atlassian = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const scopes = envConfig.getString('scopes'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = + options?.authHandler ?? atlassianDefaultAuthHandler; + + const provider = new AtlassianAuthProvider({ + clientId, + clientSecret, + scopes, + callbackUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + providerId, + callbackUrl, + }); }); + }, +}); - const authHandler: AuthHandler = - options?.authHandler ?? atlassianDefaultAuthHandler; - - const provider = new AtlassianAuthProvider({ - clientId, - clientSecret, - scopes, - callbackUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - catalogIdentityClient, - logger, - tokenIssuer, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.atlassian.create` instead + */ +export const createAtlassianProvider = atlassian.create; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index fbd57c0b76..3181314540 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -38,13 +38,11 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -54,31 +52,27 @@ export type Auth0AuthProviderOptions = OAuthProviderOptions & { domain: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class Auth0AuthProvider implements OAuthHandlers { private readonly _strategy: Auth0Strategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Auth0AuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new Auth0Strategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, domain: options.domain, + // We need passReqToCallback set to false to get params, but there's + // no matching type signature for that, so instead behold this beauty passReqToCallback: false as true, }, ( @@ -149,12 +143,7 @@ export class Auth0AuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -172,7 +161,7 @@ export class Auth0AuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -180,19 +169,10 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async info => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile does not contain an email'); - } - - const id = profile.email.split('@')[0]; - - return { id, token: '' }; -}; - -/** @public */ +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type Auth0ProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -211,58 +191,68 @@ export type Auth0ProviderOptions = { }; }; -/** @public */ -export const createAuth0Provider = ( - options?: Auth0ProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for auth0 auth + * + * @public + */ +export const auth0 = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolver = options?.signIn?.resolver; + + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + authHandler, + signInResolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + callbackUrl, + }); }); + }, +}); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; - - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - authHandler, - signInResolver, - tokenIssuer, - catalogIdentityClient, - logger, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.auth0.create` instead. + */ +export const createAuth0Provider = auth0.create; diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index a8130b3575..710d86ca3d 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -15,4 +15,4 @@ */ export { createAwsAlbProvider } from './provider'; -export type { AwsAlbProviderOptions } from './provider'; +export type { AwsAlbProviderOptions, AwsAlbResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index eb801fdd46..6ba545191b 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import express from 'express'; -import { JWT } from 'jose'; +import express from 'express'; +import { jwtVerify } from 'jose'; import { ALB_ACCESS_TOKEN_HEADER, ALB_JWT_HEADER, AwsAlbAuthProvider, } from './provider'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { makeProfileInfo } from '../../lib/passport'; +import { AuthResolverContext } from '../types'; +import { AuthenticationError } from '@backstage/errors'; -const jwtMock = JWT as jest.Mocked; +const jwtMock = jwtVerify as jest.Mocked; const mockKey = async () => { return `-----BEGIN PUBLIC KEY----- @@ -66,16 +65,6 @@ beforeEach(() => { }); describe('AwsAlbAuthProvider', () => { - const tokenIssuer: TokenIssuer = { - listPublicKeys: jest.fn(), - async issueToken(params) { - return `token-for-${params.claims.sub}`; - }, - }; - const catalogIdentityClient: CatalogIdentityClient = { - findUser: jest.fn(), - } as unknown as CatalogIdentityClient; - const mockRequest = { header: jest.fn(name => { if (name === ALB_JWT_HEADER) { @@ -115,9 +104,7 @@ describe('AwsAlbAuthProvider', () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -129,7 +116,7 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockReturnValueOnce(mockClaims); + jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims })); await provider.refresh(mockRequest, mockResponse); @@ -161,9 +148,7 @@ describe('AwsAlbAuthProvider', () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -172,18 +157,16 @@ describe('AwsAlbAuthProvider', () => { }, }); - await provider.refresh(mockRequestWithoutAccessToken, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect( + provider.refresh(mockRequestWithoutAccessToken, mockResponse), + ).rejects.toThrow(AuthenticationError); }); it('JWT is missing', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -192,18 +175,16 @@ describe('AwsAlbAuthProvider', () => { }, }); - await provider.refresh(mockRequestWithoutJwt, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect( + provider.refresh(mockRequestWithoutJwt, mockResponse), + ).rejects.toThrow(AuthenticationError); }); it('JWT is invalid', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -212,22 +193,20 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockImplementationOnce(() => { + jwtMock.mockImplementationOnce(() => { throw new Error('bad JWT'); }); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('issuer is missing', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -236,19 +215,18 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockReturnValueOnce({}); + jwtMock.mockReturnValueOnce({}); - await provider.refresh(mockRequest, mockResponse); - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('issuer is invalid', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -257,21 +235,20 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockReturnValueOnce({ + jwtMock.mockReturnValueOnce({ iss: 'INVALID_ISSUE_URL', }); - await provider.refresh(mockRequest, mockResponse); - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('SignInResolver rejects', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -280,21 +257,18 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockReturnValueOnce(mockClaims); + jwtMock.mockReturnValueOnce(mockClaims); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.end).toHaveBeenCalledTimes(1); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('AuthHandler rejects', async () => { const provider = new AwsAlbAuthProvider({ region: 'eu-west-1', issuer: 'ISSUER_URL', - logger: getVoidLogger(), - catalogIdentityClient, - tokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async () => { throw new Error(); }, @@ -303,12 +277,11 @@ describe('AwsAlbAuthProvider', () => { }, }); - jwtMock.verify.mockReturnValueOnce(mockClaims); + jwtMock.mockReturnValueOnce(mockClaims); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.end).toHaveBeenCalledTimes(1); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); }); }); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 5206dc125c..78bfb9f0ec 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -16,8 +16,8 @@ import { AuthHandler, - AuthProviderFactory, AuthProviderRouteHandlers, + AuthResolverContext, AuthResponse, SignInResolver, } from '../types'; @@ -25,15 +25,13 @@ import express from 'express'; import fetch from 'node-fetch'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; -import { Logger } from 'winston'; import NodeCache from 'node-cache'; -import { JWT } from 'jose'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { JWTHeaderParameters, jwtVerify } from 'jose'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -41,16 +39,9 @@ export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken'; type Options = { region: string; issuer?: string; - logger: Logger; authHandler: AuthHandler; signInResolver: SignInResolver; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; -}; - -export const getJWTHeaders = (input: string): AwsAlbHeaders => { - const encoded = input.split('.')[0]; - return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')); + resolverContext: AuthResolverContext; }; export type AwsAlbHeaders = { @@ -73,6 +64,7 @@ export type AwsAlbClaims = { iss: string; }; +/** @public */ export type AwsAlbResult = { fullProfile: PassportProfile; expiresInSeconds?: number; @@ -95,9 +87,7 @@ export type AwsAlbResponse = AuthResponse; export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private readonly region: string; private readonly issuer?: string; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly keyCache: NodeCache; private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; @@ -107,9 +97,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { this.issuer = options.issuer; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this.keyCache = new NodeCache({ stdTTL: 3600 }); } @@ -123,9 +111,10 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { const response = await this.handleResult(result); res.json(response); } catch (e) { - this.logger.error('Exception occurred during AWS ALB token refresh', e); - res.status(401); - res.end(); + throw new AuthenticationError( + 'Exception occurred during AWS ALB token refresh', + e, + ); } } @@ -150,9 +139,8 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } try { - const headers = getJWTHeaders(jwt); - const key = await this.getKey(headers.kid); - const claims = JWT.verify(jwt, key) as AwsAlbClaims; + const verifyResult = await jwtVerify(jwt, this.getKey); + const claims = verifyResult.payload as AwsAlbClaims; if (this.issuer && claims.iss !== this.issuer) { throw new AuthenticationError('Issuer mismatch on JWT token'); @@ -182,18 +170,13 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } private async handleResult(result: AwsAlbResult): Promise { - const context = { - tokenIssuer: this.tokenIssuer, - catalogIdentityClient: this.catalogIdentityClient, - logger: this.logger, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageIdentity = await this.signInResolver( { result, profile, }, - context, + this.resolverContext, ); return { @@ -206,22 +189,32 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { }; } - async getKey(keyId: string): Promise { - const optionalCacheKey = this.keyCache.get(keyId); + async getKey(header: JWTHeaderParameters): Promise { + if (!header.kid) { + throw new AuthenticationError('No key id was specified in header'); + } + const optionalCacheKey = this.keyCache.get(header.kid); if (optionalCacheKey) { return crypto.createPublicKey(optionalCacheKey); } - const keyText = await fetch( + const keyText: string = await fetch( `https://public-keys.auth.elb.${encodeURIComponent( this.region, - )}.amazonaws.com/${encodeURIComponent(keyId)}`, + )}.amazonaws.com/${encodeURIComponent(header.kid)}`, ).then(response => response.text()); const keyValue = crypto.createPublicKey(keyText); - this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' })); + this.keyCache.set( + header.kid, + keyValue.export({ format: 'pem', type: 'spki' }), + ); return keyValue; } } +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type AwsAlbProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -240,40 +233,58 @@ export type AwsAlbProviderOptions = { }; }; -export const createAwsAlbProvider = ( - options?: AwsAlbProviderOptions, -): AuthProviderFactory => { - return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { - const region = config.getString('region'); - const issuer = config.getOptionalString('iss'); +/** + * Auth provider integration for AWS ALB auth + * + * @public + */ +export const awsAlb = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - if (options?.signIn.resolver === undefined) { - throw new Error( - 'SignInResolver is required to use this authentication provider', - ); - } + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ config, resolverContext }) => { + const region = config.getString('region'); + const issuer = config.getOptionalString('iss'); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + if (options?.signIn.resolver === undefined) { + throw new Error( + 'SignInResolver is required to use this authentication provider', + ); + } - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); - const signInResolver = options?.signIn.resolver; + return new AwsAlbAuthProvider({ + region, + issuer, + signInResolver: options?.signIn.resolver, + authHandler, + resolverContext, + }); + }; + }, +}); - return new AwsAlbAuthProvider({ - region, - issuer, - signInResolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - logger, - }); - }; -}; +/** + * @public + * @deprecated Use `providers.awsAlb.create` instead + */ +export const createAwsAlbProvider = awsAlb.create; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts index 690729a5fb..9eeedc3d7b 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.test.ts @@ -16,9 +16,7 @@ import { BitbucketAuthProvider, BitbucketOAuthResult } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -29,19 +27,8 @@ const mockFrameHandler = jest.spyOn( describe('createBitbucketProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new BitbucketAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 1cc0e60bd8..905181333a 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -17,8 +17,6 @@ import express from 'express'; import passport, { Profile as PassportProfile } from 'passport'; import { Strategy as BitbucketStrategy } from 'passport-bitbucket-oauth2'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -38,13 +36,13 @@ import { makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -53,9 +51,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export type BitbucketOAuthResult = { @@ -87,24 +83,18 @@ export class BitbucketAuthProvider implements OAuthHandlers { private readonly _strategy: BitbucketStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new BitbucketStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, - // We need passReqToCallback set to false to get params, but there's - // no matching type signature for that, so instead behold this beauty - passReqToCallback: false as true, + passReqToCallback: false, }, ( accessToken: any, @@ -174,12 +164,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { private async handleResult(result: BitbucketOAuthResult) { result.fullProfile.avatarUrl = result.fullProfile._json!.links!.avatar!.href; - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -197,7 +182,7 @@ export class BitbucketAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -205,48 +190,10 @@ export class BitbucketAuthProvider implements OAuthHandlers { } } -export const bitbucketUsernameSignInResolver: SignInResolver< - BitbucketOAuthResult -> = async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.username) { - throw new Error('Bitbucket profile contained no Username'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'bitbucket.org/username': result.fullProfile.username, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - -export const bitbucketUserIdSignInResolver: SignInResolver< - BitbucketOAuthResult -> = async (info, ctx) => { - const { result } = info; - - if (!result.fullProfile.id) { - throw new Error('Bitbucket profile contained no User ID'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'bitbucket.org/user-id': result.fullProfile.id, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type BitbucketProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -265,54 +212,117 @@ export type BitbucketProviderOptions = { }; }; -export const createBitbucketProvider = ( - options?: BitbucketProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for BitBucket auth + * + * @public + */ +export const bitbucket = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = + options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new BitbucketAuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, + resolvers: { + /** + * Looks up the user by matching their username to the `bitbucket.org/username` annotation. + */ + usernameMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { result } = info; - const authHandler: AuthHandler = - options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + if (!result.fullProfile.username) { + throw new Error('Bitbucket profile contained no Username'); + } - const provider = new BitbucketAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - logger, - }); + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/username': result.fullProfile.username, + }, + }); + }; + }, + /** + * Looks up the user by matching their user ID to the `bitbucket.org/user-id` annotation. + */ + userIdMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { result } = info; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + if (!result.fullProfile.id) { + throw new Error('Bitbucket profile contained no User ID'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'bitbucket.org/user-id': result.fullProfile.id, + }, + }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.bitbucket.create` instead + */ +export const createBitbucketProvider = bitbucket.create; + +/** + * @public + * @deprecated Use `providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation()` instead. + */ +export const bitbucketUsernameSignInResolver = + bitbucket.resolvers.usernameMatchingUserEntityAnnotation(); + +/** + * @public + * @deprecated Use `providers.bitbucket.resolvers.userIdMatchingUserEntityAnnotation()` instead. + */ +export const bitbucketUserIdSignInResolver = + bitbucket.resolvers.userIdMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts new file mode 100644 index 0000000000..c6e9107353 --- /dev/null +++ b/plugins/auth-backend/src/providers/createAuthProviderIntegration.ts @@ -0,0 +1,44 @@ +/* + * 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 { AuthProviderFactory, SignInResolver } from './types'; + +/** + * Creates a standardized representation of an integration with a third-party + * auth provider. + * + * The returned object facilitates the creation of provider instances, and + * supplies built-in sign-in resolvers for the specific provider. + */ +export function createAuthProviderIntegration< + TCreateOptions extends unknown[], + TResolvers extends + | { + [name in string]: (...args: any[]) => SignInResolver; + }, +>(config: { + create: (...args: TCreateOptions) => AuthProviderFactory; + resolvers?: TResolvers; +}): Readonly<{ + create: (...args: TCreateOptions) => AuthProviderFactory; + // If no resolvers are defined, this receives the type `never` + resolvers: Readonly; +}> { + return Object.freeze({ + ...config, + resolvers: Object.freeze(config.resolvers ?? ({} as any)), + }); +} diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts index eb2f5c478d..b1b2a3f66e 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; +import { AuthResolverContext } from '../types'; import { GcpIapProvider } from './provider'; beforeEach(() => { @@ -27,16 +27,13 @@ describe('GcpIapProvider', () => { const authHandler = jest.fn(); const signInResolver = jest.fn(); const tokenValidator = jest.fn(); - const logger = getVoidLogger(); it('runs the happy path', async () => { const provider = new GcpIapProvider({ authHandler, signInResolver, tokenValidator, - tokenIssuer: {} as any, - catalogIdentityClient: {} as any, - logger, + resolverContext: {} as AuthResolverContext, }); // { "sub": "user:default/me", "ent": ["group:default/home"] } diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 7816f68b76..cb5d5979cd 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -16,14 +16,12 @@ import express from 'express'; import { TokenPayload } from 'google-auth-library'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; import { AuthHandler, - AuthProviderFactory, AuthProviderRouteHandlers, + AuthResolverContext, SignInResolver, } from '../types'; import { @@ -31,35 +29,24 @@ import { defaultAuthHandler, parseRequestToken, } from './helpers'; -import { - GcpIapProviderOptions, - GcpIapResponse, - GcpIapResult, - IAP_JWT_HEADER, -} from './types'; +import { GcpIapResponse, GcpIapResult, IAP_JWT_HEADER } from './types'; export class GcpIapProvider implements AuthProviderRouteHandlers { private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; private readonly tokenValidator: (token: string) => Promise; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: { authHandler: AuthHandler; signInResolver: SignInResolver; tokenValidator: (token: string) => Promise; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }) { this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; this.tokenValidator = options.tokenValidator; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; } async start() {} @@ -71,17 +58,12 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { req.header(IAP_JWT_HEADER), this.tokenValidator, ); - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageIdentity = await this.signInResolver( { profile, result }, - context, + this.resolverContext, ); const response: GcpIapResponse = { @@ -95,32 +77,49 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { } /** - * Creates an auth provider for Google Identity-Aware Proxy. + * Auth provider integration for Google Identity-Aware Proxy auth * * @public */ -export function createGcpIapProvider( - options: GcpIapProviderOptions, -): AuthProviderFactory { - return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => { - const audience = config.getString('audience'); +export const gcpIap = createAuthProviderIntegration({ + create(options: { + /** + * The profile transformation function used to verify and convert the auth + * response into the profile that will be presented to the user. The default + * implementation just provides the authenticated email that the IAP + * presented. + */ + authHandler?: AuthHandler; - const authHandler = options.authHandler ?? defaultAuthHandler; - const signInResolver = options.signIn.resolver; - const tokenValidator = createTokenValidator(audience); + /** + * Configures sign-in for this provider. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ config, resolverContext }) => { + const audience = config.getString('audience'); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + const authHandler = options.authHandler ?? defaultAuthHandler; + const signInResolver = options.signIn.resolver; + const tokenValidator = createTokenValidator(audience); - return new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - tokenIssuer, - catalogIdentityClient, - logger, - }); - }; -} + return new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + resolverContext, + }); + }; + }, +}); + +/** + * @public + * @deprecated Use `providers.gcpIap.create` instead + */ +export const createGcpIapProvider = gcpIap.create; diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 9ef1935442..6e5a022a3d 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -71,9 +71,8 @@ export type GcpIapProviderInfo = { export type GcpIapResponse = AuthResponse; /** - * Options for {@link createGcpIapProvider}. - * * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type GcpIapProviderOptions = { /** diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 206df52083..18690a986b 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -15,17 +15,11 @@ */ import { Profile as PassportProfile } from 'passport'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { - GithubAuthProvider, - GithubOAuthResult, - githubDefaultSignInResolver, -} from './provider'; +import { GithubAuthProvider, GithubOAuthResult, github } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; import { OAuthStartRequest, encodeState } from '../../lib/oauth'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -38,22 +32,13 @@ const mockFrameHandler = jest.spyOn( >; describe('GithubAuthProvider', () => { - const tokenIssuer: TokenIssuer = { - listPublicKeys: jest.fn(), - async issueToken(params) { - return `token-for-${params.claims.sub}`; - }, - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new GithubAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, - signInResolver: githubDefaultSignInResolver, + resolverContext: { + signInWithCatalogUser: jest.fn(({ entityRef }) => ({ + token: `token-for-user:${entityRef.name}`, + })), + } as unknown as AuthResolverContext, + signInResolver: github.resolvers.usernameMatchingUserEntityName(), authHandler: async ({ fullProfile }) => ({ profile: makeProfileInfo(fullProfile), }), @@ -92,8 +77,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -138,8 +122,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -182,8 +165,7 @@ describe('GithubAuthProvider', () => { }; const expected = { backstageIdentity: { - id: 'jimmymarkum', - token: 'token-for-user:default/jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -226,8 +208,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { - id: 'daveboyle', - token: 'token-for-user:default/daveboyle', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: @@ -254,6 +235,7 @@ describe('GithubAuthProvider', () => { result: { fullProfile: { id: 'ipd12039', + username: 'daveboyle', provider: 'github', displayName: 'Dave Boyle', }, @@ -271,8 +253,7 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ response: { backstageIdentity: { - id: 'ipd12039', - token: 'token-for-user:default/ipd12039', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: 'a.b.c', @@ -287,6 +268,28 @@ describe('GithubAuthProvider', () => { }); }); + it('should fail if username is not available', async () => { + mockFrameHandler.mockResolvedValueOnce({ + result: { + fullProfile: { + id: 'ipd12039', + provider: 'github', + displayName: 'Dave Boyle', + }, + accessToken: 'a.b.c', + params: { + scope: 'read:user', + expires_in: '123', + }, + }, + privateInfo: { refreshToken: 'refresh-me' }, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + 'GitHub user profile does not contain a username', + ); + }); + it('should forward a new refresh token on refresh', async () => { const mockRefreshToken = jest.spyOn( helpers, @@ -325,8 +328,7 @@ describe('GithubAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', - token: 'token-for-user:default/mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', @@ -377,8 +379,7 @@ describe('GithubAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', - token: 'token-for-user:default/mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c875f856b6..e018a6cf99 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; -import { Logger } from 'winston'; import { Profile as PassportProfile } from 'passport'; import { Strategy as GithubStrategy } from 'passport-github2'; import { @@ -32,10 +27,10 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, StateEncoder, + AuthResolverContext, } from '../types'; import { OAuthAdapter, @@ -46,8 +41,7 @@ import { encodeState, OAuthRefreshRequest, } from '../../lib/oauth'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; const ACCESS_TOKEN_PREFIX = 'access-token.'; @@ -76,27 +70,21 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; stateEncoder: StateEncoder; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly stateEncoder: StateEncoder; constructor(options: GithubAuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; this.stateEncoder = options.stateEncoder; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new GithubStrategy( { clientID: options.clientId, @@ -198,12 +186,7 @@ export class GithubAuthProvider implements OAuthHandlers { } private async handleResult(result: GithubOAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const expiresInStr = result.params.expires_in; let expiresInSeconds = @@ -217,7 +200,7 @@ export class GithubAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); // GitHub sessions last longer than Backstage sessions, so if we're using @@ -244,29 +227,10 @@ export class GithubAuthProvider implements OAuthHandlers { } } -export const githubDefaultSignInResolver: SignInResolver< - GithubOAuthResult -> = async (info, ctx) => { - const { fullProfile } = info.result; - - const userId = fullProfile.username || fullProfile.id; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GithubProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -281,7 +245,7 @@ export type GithubProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; /** @@ -303,85 +267,123 @@ export type GithubProviderOptions = { stateEncoder?: StateEncoder; }; -export const createGithubProvider = ( - options?: GithubProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig - .getOptionalString('enterpriseInstanceUrl') - ?.replace(/\/$/, ''); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for GitHub auth + * + * @public + */ +export const github = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), + /** + * The state encoder used to encode the 'state' parameter on the OAuth request. + * + * It should return a string that takes the state params (from the request), url encodes the params + * and finally base64 encodes them. + * + * Providing your own stateEncoder will allow you to add addition parameters to the state field. + * + * It is typed as follows: + * `export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;` + * + * Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail + * (These two values will be set by the req.state by default) + * + * For more information, please see the helper module in ../../oauth/helpers #readState + */ + stateEncoder?: StateEncoder; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig + .getOptionalString('enterpriseInstanceUrl') + ?.replace(/\/$/, ''); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + const stateEncoder: StateEncoder = + options?.stateEncoder ?? + (async ( + req: OAuthStartRequest, + ): Promise<{ encodedState: string }> => { + return { encodedState: encodeState(req.state) }; }); - const signInResolverFn = - options?.signIn?.resolver ?? githubDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + stateEncoder, + resolverContext, }); - const stateEncoder: StateEncoder = - options?.stateEncoder ?? - (async (req: OAuthStartRequest): Promise<{ encodedState: string }> => { - return { encodedState: encodeState(req.state) }; + return OAuthAdapter.fromConfig(globalConfig, provider, { + persistScopes: true, + providerId, + callbackUrl, }); - - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - signInResolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - stateEncoder, - logger, }); + }, + resolvers: { + /** + * Looks up the user by matching their GitHub username to the entity name. + */ + usernameMatchingUserEntityName: (): SignInResolver => { + return async (info, ctx) => { + const { fullProfile } = info.result; - return OAuthAdapter.fromConfig(globalConfig, provider, { - persistScopes: true, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + const userId = fullProfile.username; + if (!userId) { + throw new Error(`GitHub user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.github.create` instead + */ +export const createGithubProvider = github.create; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index f90de3c75b..169930786b 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { GitlabAuthProvider, gitlabDefaultSignInResolver } from './provider'; +import { + GitlabAuthProvider, + gitlabUsernameEntityNameSignInResolver, +} from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { PassportProfile } from '../../lib/passport/types'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -28,22 +29,16 @@ const mockFrameHandler = jest.spyOn( ) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('GitlabAuthProvider', () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new GitlabAuthProvider({ clientId: 'mock', clientSecret: 'mock', callbackUrl: 'mock', baseUrl: 'mock', - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: { + signInWithCatalogUser: jest.fn(async ({ entityRef }) => ({ + token: `token-for-user:${entityRef.name}`, + })), + } as unknown as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, @@ -51,8 +46,7 @@ describe('GitlabAuthProvider', () => { picture: 'http://gitlab.com/lols', }, }), - signInResolver: gitlabDefaultSignInResolver, - logger: getVoidLogger(), + signInResolver: gitlabUsernameEntityNameSignInResolver, }); it('should transform to type OAuthResponse', async () => { @@ -85,7 +79,7 @@ describe('GitlabAuthProvider', () => { }, expect: { backstageIdentity: { - id: 'jimmymarkum', + token: 'token-for-user:jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -127,7 +121,7 @@ describe('GitlabAuthProvider', () => { }, expect: { backstageIdentity: { - id: 'daveboyle', + token: 'token-for-user:daveboyle', }, providerInfo: { accessToken: @@ -189,7 +183,7 @@ describe('GitlabAuthProvider', () => { expect(result).toEqual({ response: { backstageIdentity: { - id: 'mockuser', + token: 'token-for-user:mockuser', }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 59f9a5a4cb..23867f6990 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; -import { Logger } from 'winston'; import { executeRedirectStrategy, executeFrameHandlerStrategy, @@ -31,9 +26,9 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, SignInResolver, AuthHandler, + AuthResolverContext, } from '../types'; import { OAuthAdapter, @@ -46,8 +41,7 @@ import { encodeState, OAuthResult, } from '../../lib/oauth'; -import { TokenIssuer } from '../../identity'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -57,37 +51,20 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; -export const gitlabDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile, result } = info; +export const gitlabUsernameEntityNameSignInResolver: SignInResolver< + OAuthResult +> = async (info, ctx) => { + const { result } = info; - let id = result.fullProfile.id; - - if (profile.email) { - id = profile.email.split('@')[0]; + const id = result.fullProfile.username; + if (!id) { + throw new Error(`GitLab user profile does not contain a username`); } - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id, token }; + return ctx.signInWithCatalogUser({ entityRef: { name: id } }); }; export const gitlabDefaultAuthHandler: AuthHandler = async ({ @@ -101,14 +78,10 @@ export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: GitlabAuthProviderOptions) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; @@ -179,12 +152,7 @@ export class GitlabAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -202,7 +170,7 @@ export class GitlabAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -210,6 +178,10 @@ export class GitlabAuthProvider implements OAuthHandlers { } } +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GitlabProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -227,67 +199,65 @@ export type GitlabProviderOptions = { * the catalog for a single user entity that has a matching `microsoft.com/email` annotation. */ signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createGitlabProvider = ( - options?: GitlabProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for GitLab auth + * + * @public + */ +export const gitlab = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getOptionalString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authHandler: AuthHandler = - options?.authHandler ?? gitlabDefaultAuthHandler; + const authHandler: AuthHandler = + options?.authHandler ?? gitlabDefaultAuthHandler; - const signInResolverFn = - options?.signIn?.resolver ?? gitlabDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, }); - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - authHandler, - signInResolver, - catalogIdentityClient, - logger, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.gitlab.create` instead + */ +export const createGitlabProvider = gitlab.create; diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index a69a7b0485..2e904c62ad 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -17,9 +17,7 @@ import { GoogleAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,19 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createGoogleProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new GoogleAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index e5e5698ec4..233e28f750 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,15 +14,9 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -43,12 +37,16 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderFactory, AuthHandler, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + commonByEmailLocalPartResolver, + commonByEmailResolver, +} from '../resolvers'; type PrivateInfo = { refreshToken: string; @@ -57,33 +55,25 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class GoogleAuthProvider implements OAuthHandlers { - private readonly _strategy: GoogleStrategy; + private readonly strategy: GoogleStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { - this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this._strategy = new GoogleStrategy( + this.signInResolver = options.signInResolver; + this.resolverContext = options.resolverContext; + this.strategy = new GoogleStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, - // We need passReqToCallback set to false to get params, but there's - // no matching type signature for that, so instead behold this beauty - passReqToCallback: false as true, + passReqToCallback: false, }, ( accessToken: any, @@ -109,7 +99,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { + return await executeRedirectStrategy(req, this.strategy, { accessType: 'offline', prompt: 'consent', scope: req.scope, @@ -121,7 +111,7 @@ export class GoogleAuthProvider implements OAuthHandlers { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo - >(req, this._strategy); + >(req, this.strategy); return { response: await this.handleResult(result), @@ -132,12 +122,12 @@ export class GoogleAuthProvider implements OAuthHandlers { async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy( - this._strategy, + this.strategy, req.refreshToken, req.scope, ); const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, + this.strategy, accessToken, ); @@ -152,12 +142,7 @@ export class GoogleAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -175,7 +160,7 @@ export class GoogleAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -183,69 +168,10 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -export const googleEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - -const googleDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - let userId: string; - try { - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - userId = entity.metadata.name; - } catch (error) { - ctx.logger.warn( - `Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`, - ); - userId = profile.email.split('@')[0]; - } - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type GoogleProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -260,67 +186,103 @@ export type GoogleProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createGoogleProvider = ( - options?: GoogleProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for Google auth + * + * @public + */ +export const google = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const signInResolverFn = - options?.signIn?.resolver ?? googleDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + resolverContext, }); - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - logger, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ + emailMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + if (!profile.email) { + throw new Error('Google profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.google.create` instead. + */ +export const createGoogleProvider = google.create; + +/** + * @public + * @deprecated Use `providers.google.resolvers.emailMatchingUserEntityAnnotation()` instead. + */ +export const googleEmailSignInResolver = + google.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 78814ae7e6..1f33a60e55 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -30,24 +30,25 @@ export * from './onelogin'; export * from './saml'; export * from './gcp-iap'; +export { providers } from './providers'; + export { factories as defaultAuthProviderFactories } from './factories'; -// Export the minimal interface required for implementing a -// custom Authorization Handler export type { + AuthProviderConfig, AuthProviderRouteHandlers, AuthProviderFactoryOptions, AuthProviderFactory, AuthHandler, + AuthResolverCatalogUserQuery, AuthResolverContext, AuthHandlerResult, SignInResolver, SignInInfo, CookieConfigurer, + StateEncoder, + AuthResponse, + ProfileInfo, } from './types'; -// These types are needed for a postMessage from the login pop-up -// to the frontend -export type { AuthResponse, ProfileInfo } from './types'; - export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 3a0fc5ad86..e9b4424a95 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -18,11 +18,10 @@ import { MicrosoftAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -87,19 +86,10 @@ const setupHandlers = () => { describe('createMicrosoftProvider', () => { it('should auth', async () => { setupHandlers(); - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, @@ -131,19 +121,9 @@ describe('createMicrosoftProvider', () => { it('should return the base64 encoded photo data of the profile', async () => { setupHandlers(); - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new MicrosoftAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 98f94811bd..0a4b67d667 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,15 +14,9 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; import { encodeState, OAuthAdapter, @@ -43,11 +37,16 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + commonByEmailLocalPartResolver, + commonByEmailResolver, +} from '../resolvers'; import { Logger } from 'winston'; import fetch from 'node-fetch'; @@ -58,9 +57,8 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; logger: Logger; + resolverContext: AuthResolverContext; authorizationUrl?: string; tokenUrl?: string; }; @@ -69,16 +67,14 @@ export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; this.logger = options.logger; - this.catalogIdentityClient = options.catalogIdentityClient; + this.resolverContext = options.resolverContext; this._strategy = new MicrosoftStrategy( { @@ -87,7 +83,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { callbackURL: options.callbackUrl, authorizationURL: options.authorizationUrl, tokenURL: options.tokenUrl, - passReqToCallback: false as true, + passReqToCallback: false, }, ( accessToken: any, @@ -147,12 +143,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { const photo = await this.getUserPhoto(result.accessToken); result.fullProfile.photos = photo ? [{ value: photo }] : undefined; - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -170,87 +161,39 @@ export class MicrosoftAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } return response; } - private getUserPhoto(accessToken: string): Promise { - return new Promise(resolve => { - fetch('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', { - headers: { - Authorization: `Bearer ${accessToken}`, + private async getUserPhoto(accessToken: string): Promise { + try { + const res = await fetch( + 'https://graph.microsoft.com/v1.0/me/photos/48x48/$value', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, }, - }) - .then(response => response.arrayBuffer()) - .then(arrayBuffer => { - const imageUrl = `data:image/jpeg;base64,${Buffer.from( - arrayBuffer, - ).toString('base64')}`; - resolve(imageUrl); - }) - .catch(error => { - this.logger.warn( - `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, - ); - // User profile photo is optional, ignore errors and resolve undefined - resolve(undefined); - }); - }); + ); + const data = await res.buffer(); + + return `data:image/jpeg;base64,${data.toString('base64')}`; + } catch (error) { + this.logger.warn( + `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, + ); + return undefined; + } } } -export const microsoftEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Microsoft profile contained no email'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'microsoft.com/email': profile.email, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - -export const microsoftDefaultSignInResolver: SignInResolver< - OAuthResult -> = async (info, ctx) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type MicrosoftProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -265,73 +208,110 @@ export type MicrosoftProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createMicrosoftProvider = ( - options?: MicrosoftProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantId = envConfig.getString('tenantId'); +/** + * Auth provider integration for Microsoft auth + * + * @public + */ +export const microsoft = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, logger, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantId = envConfig.getString('tenantId'); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const signInResolverFn = - options?.signIn?.resolver ?? microsoftDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + authHandler, + signInResolver: options?.signIn?.resolver, logger, + resolverContext, }); - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - authHandler, - signInResolver, - catalogIdentityClient, - logger, - tokenIssuer, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + /** + * Looks up the user by matching their email to the `microsoft.com/email` annotation. + */ + emailMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + if (!profile.email) { + throw new Error('Microsoft profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'microsoft.com/email': profile.email, + }, + }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.microsoft.create` instead + */ +export const createMicrosoftProvider = microsoft.create; + +/** + * @public + * @deprecated Use `providers.microsoft.resolvers.emailMatchingUserEntityAnnotation()` instead. + */ +export const microsoftEmailSignInResolver = + microsoft.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts index 61f08bc535..d258c17e61 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts @@ -15,28 +15,18 @@ */ jest.mock('jose', () => ({ - JWT: { - decode: jest.fn(), - }, + decodeJwt: jest.fn(), })); jest.mock('@backstage/catalog-client'); +import { AuthenticationError } from '@backstage/errors'; import express from 'express'; -import { JWT } from 'jose'; +import * as jose from 'jose'; import { Logger } from 'winston'; -import { - AuthHandler, - SignInResolver, - AuthProviderFactoryOptions, -} from '../types'; - -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity/types'; - +import { AuthHandler, AuthResolverContext, SignInResolver } from '../types'; import { createOauth2ProxyProvider, Oauth2ProxyAuthProvider, - Oauth2ProxyProviderOptions, OAuth2ProxyResult, OAUTH2_PROXY_JWT_HEADER, } from './provider'; @@ -53,12 +43,14 @@ describe('Oauth2ProxyAuthProvider', () => { let authHandler: jest.MockedFunction>>; let mockResponse: jest.Mocked; let mockRequest: jest.Mocked; - let JWTMock: jest.Mocked; + let mockJwtDecode: jest.MockedFunction; beforeEach(() => { jest.resetAllMocks(); - JWTMock = JWT as jest.Mocked; + mockJwtDecode = jose.decodeJwt as jest.MockedFunction< + typeof jose.decodeJwt + >; authHandler = jest.fn(); signInResolver = jest.fn(); logger = { error: jest.fn() } as unknown as jest.Mocked; @@ -76,10 +68,10 @@ describe('Oauth2ProxyAuthProvider', () => { provider = new Oauth2ProxyAuthProvider({ authHandler, - logger, signInResolver, - catalogIdentityClient: {} as CatalogIdentityClient, - tokenIssuer: {} as TokenIssuer, + resolverContext: { + _: 'resolver-context', + } as unknown as AuthResolverContext, }); }); @@ -103,17 +95,17 @@ describe('Oauth2ProxyAuthProvider', () => { it('should throw an error when auth header is missing', async () => { mockRequest.header.mockReturnValue(undefined); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('should throw an error if the bearer token is invalid', async () => { mockRequest.header.mockReturnValue('Basic asdf='); - await provider.refresh(mockRequest, mockResponse); - - expect(mockResponse.status).toHaveBeenCalledWith(401); + await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow( + AuthenticationError, + ); }); it('should return if auth header is set and valid', async () => { @@ -128,7 +120,7 @@ describe('Oauth2ProxyAuthProvider', () => { await provider.refresh(mockRequest, mockResponse); expect(mockRequest.header).toBeCalledWith(OAUTH2_PROXY_JWT_HEADER); - expect(JWTMock.decode).toHaveBeenCalledWith('token'); + expect(mockJwtDecode).toHaveBeenCalledWith('token'); expect(mockResponse.json).toHaveBeenCalled(); }); @@ -144,7 +136,7 @@ describe('Oauth2ProxyAuthProvider', () => { token: mockToken, }); authHandler.mockResolvedValue({ profile: profile }); - JWTMock.decode.mockReturnValue(decodedToken as any); + mockJwtDecode.mockReturnValue(decodedToken as any); await provider.refresh(mockRequest, mockResponse); @@ -156,7 +148,7 @@ describe('Oauth2ProxyAuthProvider', () => { fullProfile: decodedToken, }, }, - { catalogIdentityClient: {}, logger, tokenIssuer: {} }, + { _: 'resolver-context' }, ); expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { @@ -187,22 +179,19 @@ describe('Oauth2ProxyAuthProvider', () => { }); it('should create a valid provider', async () => { - const providerOptions = { + const factory = createOauth2ProxyProvider({ authHandler, signIn: { resolver: signInResolver }, - } as Oauth2ProxyProviderOptions; - const factoryOptions = { + }); + const handler = factory({ logger, catalogApi: {}, tokenIssuer: {}, - } as unknown as AuthProviderFactoryOptions; - - const factory = createOauth2ProxyProvider(providerOptions); - const handler = factory(factoryOptions); + } as any); await handler.refresh!(mockRequest, mockResponse); expect(mockRequest.header).toBeCalledWith(OAUTH2_PROXY_JWT_HEADER); - expect(JWTMock.decode).toHaveBeenCalledWith('token'); + expect(mockJwtDecode).toHaveBeenCalledWith('token'); expect(mockResponse.json).toHaveBeenCalled(); }); }); diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 8c4dcc3249..acef4a74ef 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -15,20 +15,18 @@ */ import express from 'express'; -import { Logger } from 'winston'; import { AuthenticationError } from '@backstage/errors'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { AuthHandler, SignInResolver, - AuthProviderFactory, AuthProviderRouteHandlers, AuthResponse, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { JWT } from 'jose'; -import { TokenIssuer } from '../../identity/types'; +import { decodeJwt } from 'jose'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; export const OAUTH2_PROXY_JWT_HEADER = 'X-OAUTH2-PROXY-ID-TOKEN'; @@ -51,9 +49,8 @@ export type OAuth2ProxyResult = { }; /** - * Options for the oauth2-proxy provider factory - * * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type Oauth2ProxyProviderOptions = { /** @@ -73,28 +70,22 @@ export type Oauth2ProxyProviderOptions = { }; interface Options { - logger: Logger; + resolverContext: AuthResolverContext; signInResolver: SignInResolver>; authHandler: AuthHandler>; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; } export class Oauth2ProxyAuthProvider implements AuthProviderRouteHandlers { - private readonly logger: Logger; - private readonly catalogIdentityClient: CatalogIdentityClient; + private readonly resolverContext: AuthResolverContext; private readonly signInResolver: SignInResolver< OAuth2ProxyResult >; private readonly authHandler: AuthHandler>; - private readonly tokenIssuer: TokenIssuer; constructor(options: Options) { - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; - this.tokenIssuer = options.tokenIssuer; + this.resolverContext = options.resolverContext; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; } @@ -106,17 +97,10 @@ export class Oauth2ProxyAuthProvider async refresh(req: express.Request, res: express.Response): Promise { try { const result = this.getResult(req); - const response = await this.handleResult(result); - res.json(response); } catch (e) { - this.logger.error( - `Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, - e, - ); - res.status(401); - res.end(); + throw new AuthenticationError('Refresh failed', e); } } @@ -127,20 +111,14 @@ export class Oauth2ProxyAuthProvider private async handleResult( result: OAuth2ProxyResult, ): Promise> { - const ctx = { - logger: this.logger, - tokenIssuer: this.tokenIssuer, - catalogIdentityClient: this.catalogIdentityClient, - }; - - const { profile } = await this.authHandler(result, ctx); + const { profile } = await this.authHandler(result, this.resolverContext); const backstageSignInResult = await this.signInResolver( { result, profile, }, - ctx, + this.resolverContext, ); return { @@ -164,7 +142,7 @@ export class Oauth2ProxyAuthProvider ); } - const decodedJWT = JWT.decode(jwt) as unknown as JWTPayload; + const decodedJWT = decodeJwt(jwt) as unknown as JWTPayload; return { fullProfile: decodedJWT, @@ -174,26 +152,41 @@ export class Oauth2ProxyAuthProvider } /** - * Factory function for oauth2-proxy auth provider + * Auth provider integration for oauth2-proxy auth * * @public */ -export const createOauth2ProxyProvider = - ( - options: Oauth2ProxyProviderOptions, - ): AuthProviderFactory => - ({ catalogApi, logger, tokenIssuer, tokenManager }) => { - const signInResolver = options.signIn.resolver; - const authHandler = options.authHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); - return new Oauth2ProxyAuthProvider({ - logger, - signInResolver, - authHandler, - tokenIssuer, - catalogIdentityClient, - }); - }; +export const oauth2Proxy = createAuthProviderIntegration({ + create(options: { + /** + * Configure an auth handler to generate a profile for the user. + */ + authHandler: AuthHandler>; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver>; + }; + }) { + return ({ resolverContext }) => { + const signInResolver = options.signIn.resolver; + const authHandler = options.authHandler; + return new Oauth2ProxyAuthProvider({ + resolverContext, + signInResolver, + authHandler, + }); + }; + }, +}); + +/** + * @public + * @deprecated Use `providers.oauth2Proxy.create` instead + */ +export const createOauth2ProxyProvider = oauth2Proxy.create; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.test.ts b/plugins/auth-backend/src/providers/oauth2/provider.test.ts index 7030d91343..23e08dd729 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.test.ts @@ -17,9 +17,7 @@ import { OAuth2AuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,19 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createOAuth2Provider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new OAuth2AuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index d09337f5ad..8e0250cf2f 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; @@ -42,13 +38,11 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -57,12 +51,10 @@ type PrivateInfo = { export type OAuth2AuthProviderOptions = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; authorizationUrl: string; tokenUrl: string; scope?: string; - logger: Logger; + resolverContext: AuthResolverContext; includeBasicAuth?: boolean; }; @@ -70,16 +62,12 @@ export class OAuth2AuthProvider implements OAuthHandlers { private readonly _strategy: OAuth2Strategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: OAuth2AuthProviderOptions) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new OAuth2Strategy( { @@ -88,7 +76,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { callbackURL: options.callbackUrl, authorizationURL: options.authorizationUrl, tokenURL: options.tokenUrl, - passReqToCallback: false as true, + passReqToCallback: false, scope: options.scope, customHeaders: options.includeBasicAuth ? { @@ -167,12 +155,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -190,7 +173,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -202,109 +185,77 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } -export const oAuth2DefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OAuth2ProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createOAuth2Provider = ( - options?: OAuth2ProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); - const scope = envConfig.getOptionalString('scope'); - const includeBasicAuth = envConfig.getOptionalBoolean('includeBasicAuth'); - const disableRefresh = - envConfig.getOptionalBoolean('disableRefresh') ?? false; +/** + * Auth provider integration for generic OAuth2 auth + * + * @public + */ +export const oauth2 = createAuthProviderIntegration({ + create(options?: { + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); + const includeBasicAuth = + envConfig.getOptionalBoolean('includeBasicAuth'); + const disableRefresh = + envConfig.getOptionalBoolean('disableRefresh') ?? false; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const signInResolverFn = - options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + signInResolver: options?.signIn?.resolver, + authHandler, + authorizationUrl, + tokenUrl, + scope, + includeBasicAuth, + resolverContext, }); - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - tokenIssuer, - catalogIdentityClient, - callbackUrl, - signInResolver, - authHandler, - authorizationUrl, - tokenUrl, - scope, - logger, - includeBasicAuth, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.oauth2.create` instead + */ +export const createOAuth2Provider = oauth2.create; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index d9902b600e..59a717f132 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -18,14 +18,13 @@ import { Config, ConfigReader } from '@backstage/config'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import express from 'express'; import { Session } from 'express-session'; -import { JWK, JWT } from 'jose'; +import { UnsecuredJWT } from 'jose'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OAuthAdapter } from '../../lib/oauth'; -import { AuthProviderFactoryOptions } from '../types'; import { createOidcProvider, OidcAuthProvider, Options } from './provider'; -import { getVoidLogger } from '@backstage/backend-common'; +import { AuthResolverContext } from '../types'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -43,23 +42,13 @@ const issuerMetadata = { request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; -const catalogIdentityClient = { - findUser: jest.fn(), -}; -const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), -}; - const clientMetadata: Options = { authHandler: async input => ({ profile: { displayName: input.userinfo.email, }, }), - catalogIdentityClient: catalogIdentityClient as unknown as any, - logger: getVoidLogger(), - tokenIssuer: tokenIssuer as unknown as any, + resolverContext: {} as AuthResolverContext, callbackUrl: 'https://oidc.test/callback', clientId: 'testclientid', clientSecret: 'testclientsecret', @@ -101,13 +90,18 @@ describe('OidcAuthProvider', () => { }); it('OidcAuthProvider#handler successfully invokes the oidc endpoints', async () => { - const jwt = { - sub: 'alice', - iss: 'https://oidc.test', - iat: Date.now(), - aud: clientMetadata.clientId, - exp: Date.now() + 10000, - }; + const sub = 'alice'; + const iss = 'https://oidc.test'; + const iat = Date.now(); + const aud = clientMetadata.clientId; + const exp = Date.now() + 10000; + const jwt = await new UnsecuredJWT({ iss, sub, aud, iat, exp }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .encode(); const requestSequence: Array = []; // The array of expected requests executed by the provider handler @@ -125,7 +119,7 @@ describe('OidcAuthProvider', () => { method: 'post', url: 'https://oidc.test/as/token.oauth2', payload: { - id_token: JWT.sign(jwt, JWK.None), + id_token: jwt, access_token: 'test', authorization_signed_response_alg: 'HS256', }, @@ -178,14 +172,13 @@ describe('OidcAuthProvider', () => { metadataUrl: 'https://oidc.test/.well-known/openid-configuration', }, } as any); - const options = { + const provider = createOidcProvider()({ globalConfig: { appUrl: 'https://oidc.test', baseUrl: 'https://oidc.test', }, config, - } as AuthProviderFactoryOptions; - const provider = createOidcProvider()(options) as OAuthAdapter; + } as any) as OAuthAdapter; expect(provider.start).toBeDefined(); // Cast provider as any here to be able to inspect private members await (provider as any).handlers.get('testEnv').handlers.implementation; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index b4f34cd127..aba9048cc7 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { Client, @@ -43,13 +39,11 @@ import { } from '../../lib/passport'; import { AuthHandler, - AuthProviderFactory, + AuthResolverContext, RedirectInfo, SignInResolver, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken?: string; @@ -76,9 +70,7 @@ export type Options = OAuthProviderOptions & { tokenSignedResponseAlg?: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OidcAuthProvider implements OAuthHandlers { @@ -88,9 +80,7 @@ export class OidcAuthProvider implements OAuthHandlers { private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.implementation = this.setupStrategy(options); @@ -98,9 +88,7 @@ export class OidcAuthProvider implements OAuthHandlers { this.prompt = options.prompt; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; } async start(req: OAuthStartRequest): Promise { @@ -158,7 +146,7 @@ export class OidcAuthProvider implements OAuthHandlers { const strategy = new OidcStrategy( { client, - passReqToCallback: false as true, + passReqToCallback: false, }, ( tokenset: TokenSet, @@ -186,12 +174,7 @@ export class OidcAuthProvider implements OAuthHandlers { // Use this function to grab the user profile info from the token // Then populate the profile with it private async handleResult(result: OidcAuthResult): Promise { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { idToken: result.tokenset.id_token, @@ -207,7 +190,7 @@ export class OidcAuthProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -215,122 +198,80 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const oidcDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Profile contained no email'); - } - - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - /** - * OIDC provider callback options. An auth handler and a sign in resolver - * can be passed while creating a OIDC provider. - * - * authHandler : called after sign in was successful, a new object must be returned which includes a profile - * signInResolver: called after sign in was successful, expects to return a new {@link @backstage/plugin-auth-node#BackstageSignInResult} - * - * Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly - * otherwise it throws an error - * * @public + * @deprecated This type has been inlined into the create method and will be removed. */ export type OidcProviderOptions = { authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createOidcProvider = ( - options?: OidcProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const metadataUrl = envConfig.getString('metadataUrl'); - const tokenSignedResponseAlg = envConfig.getOptionalString( - 'tokenSignedResponseAlg', - ); - const scope = envConfig.getOptionalString('scope'); - const prompt = envConfig.getOptionalString('prompt'); - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); +/** + * Auth provider integration for generic OpenID Connect auth + * + * @public + */ +export const oidc = createAuthProviderIntegration({ + create(options?: { + authHandler?: AuthHandler; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ userinfo }) => ({ - profile: { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }, - }); - const signInResolverFn = - options?.signIn?.resolver ?? oidcDefaultSignInResolver; - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + signIn?: { + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const metadataUrl = envConfig.getString('metadataUrl'); + const tokenSignedResponseAlg = envConfig.getOptionalString( + 'tokenSignedResponseAlg', + ); + const scope = envConfig.getOptionalString('scope'); + const prompt = envConfig.getOptionalString('prompt'); + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ userinfo }) => ({ + profile: { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }, + }); + + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenSignedResponseAlg, + metadataUrl, + scope, + prompt, + signInResolver: options?.signIn?.resolver, + authHandler, + resolverContext, }); - const provider = new OidcAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenSignedResponseAlg, - metadataUrl, - scope, - prompt, - signInResolver, - authHandler, - logger, - tokenIssuer, - catalogIdentityClient, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.oidc.create` instead + */ +export const createOidcProvider = oidc.create; diff --git a/plugins/auth-backend/src/providers/okta/provider.test.ts b/plugins/auth-backend/src/providers/okta/provider.test.ts index 939981e81c..468c4cdcc8 100644 --- a/plugins/auth-backend/src/providers/okta/provider.test.ts +++ b/plugins/auth-backend/src/providers/okta/provider.test.ts @@ -17,9 +17,7 @@ import { OktaAuthProvider } from './provider'; import * as helpers from '../../lib/passport/PassportStrategyHelper'; import { OAuthResult } from '../../lib/oauth'; -import { getVoidLogger } from '@backstage/backend-common'; -import { TokenIssuer } from '../../identity/types'; -import { CatalogIdentityClient } from '../../lib/catalog'; +import { AuthResolverContext } from '../types'; const mockFrameHandler = jest.spyOn( helpers, @@ -30,19 +28,8 @@ const mockFrameHandler = jest.spyOn( describe('createOktaProvider', () => { it('should auth', async () => { - const tokenIssuer = { - issueToken: jest.fn(), - listPublicKeys: jest.fn(), - }; - const catalogIdentityClient = { - findUser: jest.fn(), - }; - const provider = new OktaAuthProvider({ - logger: getVoidLogger(), - catalogIdentityClient: - catalogIdentityClient as unknown as CatalogIdentityClient, - tokenIssuer: tokenIssuer as unknown as TokenIssuer, + resolverContext: {} as AuthResolverContext, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 26f586c578..4e05ff4487 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { OAuthAdapter, @@ -41,15 +37,17 @@ import { PassportDoneCallback, } from '../../lib/passport'; import { - AuthProviderFactory, AuthHandler, RedirectInfo, SignInResolver, + AuthResolverContext, } from '../types'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { + commonByEmailLocalPartResolver, + commonByEmailResolver, +} from '../resolvers'; import { StateStore } from 'passport-oauth2'; -import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; -import { TokenIssuer } from '../../identity'; -import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -59,18 +57,14 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OktaAuthProvider implements OAuthHandlers { - private readonly _strategy: any; - private readonly _signInResolver?: SignInResolver; - private readonly _authHandler: AuthHandler; - private readonly _tokenIssuer: TokenIssuer; - private readonly _catalogIdentityClient: CatalogIdentityClient; - private readonly _logger: Logger; + private readonly strategy: any; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly resolverContext: AuthResolverContext; /** * Due to passport-okta-oauth forcing options.state = true, @@ -80,7 +74,7 @@ export class OktaAuthProvider implements OAuthHandlers { * passport-oauth2, which is the StateStore implementation used when options.state = false, * allowing us to avoid using express-session in order to integrate with Okta. */ - private _store: StateStore = { + private store: StateStore = { store(_req: express.Request, cb: any) { cb(null, null); }, @@ -90,20 +84,18 @@ export class OktaAuthProvider implements OAuthHandlers { }; constructor(options: OktaAuthProviderOptions) { - this._signInResolver = options.signInResolver; - this._authHandler = options.authHandler; - this._tokenIssuer = options.tokenIssuer; - this._catalogIdentityClient = options.catalogIdentityClient; - this._logger = options.logger; + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.resolverContext = options.resolverContext; - this._strategy = new OktaStrategy( + this.strategy = new OktaStrategy( { clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, audience: options.audience, - passReqToCallback: false as true, - store: this._store, + passReqToCallback: false, + store: this.store, response_type: 'code', }, ( @@ -130,7 +122,7 @@ export class OktaAuthProvider implements OAuthHandlers { } async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { + return await executeRedirectStrategy(req, this.strategy, { accessType: 'offline', prompt: 'consent', scope: req.scope, @@ -142,7 +134,7 @@ export class OktaAuthProvider implements OAuthHandlers { const { result, privateInfo } = await executeFrameHandlerStrategy< OAuthResult, PrivateInfo - >(req, this._strategy); + >(req, this.strategy); return { response: await this.handleResult(result), @@ -153,13 +145,13 @@ export class OktaAuthProvider implements OAuthHandlers { async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy( - this._strategy, + this.strategy, req.refreshToken, req.scope, ); const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, + this.strategy, accessToken, ); @@ -174,12 +166,7 @@ export class OktaAuthProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this._logger, - catalogIdentityClient: this._catalogIdentityClient, - tokenIssuer: this._tokenIssuer, - }; - const { profile } = await this._authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -191,13 +178,13 @@ export class OktaAuthProvider implements OAuthHandlers { profile, }; - if (this._signInResolver) { - response.backstageIdentity = await this._signInResolver( + if (this.signInResolver) { + response.backstageIdentity = await this.signInResolver( { result, profile, }, - context, + this.resolverContext, ); } @@ -205,57 +192,10 @@ export class OktaAuthProvider implements OAuthHandlers { } } -export const oktaEmailSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Okta profile contained no email'); - } - - const entity = await ctx.catalogIdentityClient.findUser({ - annotations: { - 'okta.com/email': profile.email, - }, - }); - - const claims = getEntityClaims(entity); - const token = await ctx.tokenIssuer.issueToken({ claims }); - - return { id: entity.metadata.name, entity, token }; -}; - -export const oktaDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Okta profile contained no email'); - } - - // TODO(Rugvip): Hardcoded to the local part of the email for now - const userId = profile.email.split('@')[0]; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: userId, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id: userId, token }; -}; - +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OktaProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -270,76 +210,112 @@ export type OktaProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -export const createOktaProvider = ( - _options?: OktaProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for Okta auth + * + * @public + */ +export const okta = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - // This is a safe assumption as `passport-okta-oauth` uses the audience - // as the base for building the authorization, token, and user info URLs. - // https://github.com/fischerdan/passport-okta-oauth/blob/ea9ac42d/lib/passport-okta-oauth/oauth2.js#L12-L14 - if (!audience.startsWith('https://')) { - throw new Error("URL for 'audience' must start with 'https://'."); - } + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); + // This is a safe assumption as `passport-okta-oauth` uses the audience + // as the base for building the authorization, token, and user info URLs. + // https://github.com/fischerdan/passport-okta-oauth/blob/ea9ac42d/lib/passport-okta-oauth/oauth2.js#L12-L14 + if (!audience.startsWith('https://')) { + throw new Error("URL for 'audience' must start with 'https://'."); + } - const authHandler: AuthHandler = _options?.authHandler - ? _options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); - const signInResolverFn = - _options?.signIn?.resolver ?? oktaDefaultSignInResolver; - - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, }); - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - authHandler, - signInResolver, - tokenIssuer, - catalogIdentityClient, - logger, + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, + resolvers: { + /** + * Looks up the user by matching their email local part to the entity name. + */ + emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, + /** + * Looks up the user by matching their email to the `okta.com/email` annotation. + */ + emailMatchingUserEntityAnnotation(): SignInResolver { + return async (info, ctx) => { + const { profile } = info; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; + if (!profile.email) { + throw new Error('Okta profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'okta.com/email': profile.email, + }, + }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.okta.create` instead + */ +export const createOktaProvider = okta.create; + +/** + * @public + * @deprecated Use `providers.okta.resolvers.emailMatchingUserEntityAnnotation()` instead. + */ +export const oktaEmailSignInResolver = + okta.resolvers.emailMatchingUserEntityAnnotation(); diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index e14e8548a0..7dcc6455ac 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -38,13 +38,11 @@ import { } from '../../lib/passport'; import { RedirectInfo, - AuthProviderFactory, AuthHandler, SignInResolver, + AuthResolverContext, } from '../types'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; -import { TokenIssuer } from '../../identity'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; type PrivateInfo = { refreshToken: string; @@ -54,32 +52,26 @@ export type Options = OAuthProviderOptions & { issuer: string; signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; constructor(options: Options) { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this._strategy = new OneLoginStrategy( { issuer: options.issuer, clientID: options.clientId, clientSecret: options.clientSecret, callbackURL: options.callbackUrl, - passReqToCallback: false as true, + passReqToCallback: false, }, ( accessToken: any, @@ -148,12 +140,7 @@ export class OneLoginProvider implements OAuthHandlers { } private async handleResult(result: OAuthResult) { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: OAuthResponse = { providerInfo: { @@ -171,7 +158,7 @@ export class OneLoginProvider implements OAuthHandlers { result, profile, }, - context, + this.resolverContext, ); } @@ -179,19 +166,10 @@ export class OneLoginProvider implements OAuthHandlers { } } -const defaultSignInResolver: SignInResolver = async info => { - const { profile } = info; - - if (!profile.email) { - throw new Error('OIDC profile contained no email'); - } - - const id = profile.email.split('@')[0]; - - return { id, token: '' }; -}; - -/** @public */ +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type OneLoginProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -210,58 +188,66 @@ export type OneLoginProviderOptions = { }; }; -/** @public */ -export const createOneLoginProvider = ( - options?: OneLoginProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const issuer = envConfig.getString('issuer'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; +/** + * Auth provider integration for OneLogin auth + * + * @public + */ +export const onelogin = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const issuer = envConfig.getString('issuer'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const provider = new OneLoginProvider({ + clientId, + clientSecret, + callbackUrl, + issuer, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + callbackUrl, + }); }); + }, +}); - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver; - - const provider = new OneLoginProvider({ - clientId, - clientSecret, - callbackUrl, - issuer, - authHandler, - signInResolver, - tokenIssuer, - catalogIdentityClient, - logger, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - callbackUrl, - }); - }); -}; +/** + * @public + * @deprecated Use `providers.onelogin.create` instead + */ +export const createOneLoginProvider = onelogin.create; diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts new file mode 100644 index 0000000000..3d49e2bbbb --- /dev/null +++ b/plugins/auth-backend/src/providers/providers.ts @@ -0,0 +1,54 @@ +/* + * 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 { atlassian } from './atlassian/provider'; +import { auth0 } from './auth0/provider'; +import { awsAlb } from './aws-alb/provider'; +import { bitbucket } from './bitbucket/provider'; +import { gcpIap } from './gcp-iap/provider'; +import { github } from './github/provider'; +import { gitlab } from './gitlab/provider'; +import { google } from './google/provider'; +import { microsoft } from './microsoft/provider'; +import { oauth2 } from './oauth2/provider'; +import { oauth2Proxy } from './oauth2-proxy/provider'; +import { oidc } from './oidc/provider'; +import { okta } from './okta/provider'; +import { onelogin } from './onelogin/provider'; +import { saml } from './saml/provider'; + +/** + * All built-in auth provider integrations. + * + * @public + */ +export const providers = Object.freeze({ + atlassian, + auth0, + awsAlb, + bitbucket, + gcpIap, + github, + gitlab, + google, + microsoft, + oauth2, + oauth2Proxy, + oidc, + okta, + onelogin, + saml, +}); diff --git a/plugins/auth-backend/src/providers/resolvers.ts b/plugins/auth-backend/src/providers/resolvers.ts new file mode 100644 index 0000000000..129c29c5e4 --- /dev/null +++ b/plugins/auth-backend/src/providers/resolvers.ts @@ -0,0 +1,58 @@ +/* + * 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 { SignInResolver } from './types'; + +/** + * A common sign-in resolver that looks up the user using the local part of + * their email address as the entity name. + */ +export const commonByEmailLocalPartResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Login failed, user profile does not contain an email'); + } + const [localPart] = profile.email.split('@'); + + return ctx.signInWithCatalogUser({ + entityRef: { name: localPart }, + }); +}; + +/** + * A common sign-in resolver that looks up the user using their email address + * as email of the entity. + */ +export const commonByEmailResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Login failed, user profile does not contain an email'); + } + + return ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': profile.email, + }, + }); +}; diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index 0aea660941..8d36e17d29 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { createSamlProvider } from './provider'; +export { + createSamlProvider, + samlNameIdEntityNameSignInResolver, +} from './provider'; export type { SamlProviderOptions, SamlAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 4458d423ef..4f5076b410 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; import express from 'express'; import { SamlConfig } from 'passport-saml/lib/passport-saml/types'; import { @@ -32,16 +28,14 @@ import { } from '../../lib/passport'; import { AuthProviderRouteHandlers, - AuthProviderFactory, AuthHandler, SignInResolver, AuthResponse, + AuthResolverContext, } from '../types'; import { postMessageResponse } from '../../lib/flow'; -import { TokenIssuer } from '../../identity/types'; -import { isError } from '@backstage/errors'; -import { CatalogIdentityClient } from '../../lib/catalog'; -import { Logger } from 'winston'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthenticationError, isError } from '@backstage/errors'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; /** @public */ @@ -52,9 +46,7 @@ export type SamlAuthResult = { type Options = SamlConfig & { signInResolver?: SignInResolver; authHandler: AuthHandler; - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; - logger: Logger; + resolverContext: AuthResolverContext; appUrl: string; }; @@ -62,18 +54,14 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly tokenIssuer: TokenIssuer; - private readonly catalogIdentityClient: CatalogIdentityClient; - private readonly logger: Logger; + private readonly resolverContext: AuthResolverContext; private readonly appUrl: string; constructor(options: Options) { this.appUrl = options.appUrl; this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; - this.tokenIssuer = options.tokenIssuer; - this.catalogIdentityClient = options.catalogIdentityClient; - this.logger = options.logger; + this.resolverContext = options.resolverContext; this.strategy = new SamlStrategy({ ...options }, (( fullProfile: SamlProfile, done: PassportDoneCallback, @@ -97,18 +85,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res: express.Response, ): Promise { try { - const context = { - logger: this.logger, - catalogIdentityClient: this.catalogIdentityClient, - tokenIssuer: this.tokenIssuer, - }; - const { result } = await executeFrameHandlerStrategy( req, this.strategy, ); - const { profile } = await this.authHandler(result, context); + const { profile } = await this.authHandler(result, this.resolverContext); const response: AuthResponse<{}> = { profile, @@ -121,7 +103,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { result, profile, }, - context, + this.resolverContext, ); response.backstageIdentity = @@ -148,31 +130,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } -const samlDefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const id = info.result.fullProfile.nameID; - - const entityRef = stringifyEntityRef({ - kind: 'User', - namespace: DEFAULT_NAMESPACE, - name: id, - }); - - const token = await ctx.tokenIssuer.issueToken({ - claims: { - sub: entityRef, - ent: [entityRef], - }, - }); - - return { id, token }; -}; - type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -/** @public */ +/** + * @public + * @deprecated This type has been inlined into the create method and will be removed. + */ export type SamlProviderOptions = { /** * The profile transformation function used to verify and convert the auth response @@ -187,70 +150,96 @@ export type SamlProviderOptions = { /** * Maps an auth result to a Backstage identity for the user. */ - resolver?: SignInResolver; + resolver: SignInResolver; }; }; -/** @public */ -export const createSamlProvider = ( - options?: SamlProviderOptions, -): AuthProviderFactory => { - return ({ - providerId, - globalConfig, - config, - tokenIssuer, - tokenManager, - catalogApi, - logger, - }) => { - const catalogIdentityClient = new CatalogIdentityClient({ - catalogApi, - tokenManager, - }); +/** + * Auth provider integration for SAML auth + * + * @public + */ +export const saml = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: { - email: fullProfile.email, - displayName: fullProfile.displayName, - }, - }); + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => { + const authHandler: AuthHandler = options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: { + email: fullProfile.email, + displayName: fullProfile.displayName, + }, + }); - const signInResolverFn = - options?.signIn?.resolver ?? samlDefaultSignInResolver; + return new SamlAuthProvider({ + callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, + entryPoint: config.getString('entryPoint'), + logoutUrl: config.getOptionalString('logoutUrl'), + audience: config.getOptionalString('audience'), + issuer: config.getString('issuer'), + cert: config.getString('cert'), + privateKey: config.getOptionalString('privateKey'), + authnContext: config.getOptionalStringArray('authnContext'), + identifierFormat: config.getOptionalString('identifierFormat'), + decryptionPvk: config.getOptionalString('decryptionPvk'), + signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as + | SignatureAlgorithm + | undefined, + digestAlgorithm: config.getOptionalString('digestAlgorithm'), + acceptedClockSkewMs: config.getOptionalNumber('acceptedClockSkewMs'), - const signInResolver: SignInResolver = info => - signInResolverFn(info, { - catalogIdentityClient, - tokenIssuer, - logger, + appUrl: globalConfig.appUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, }); + }; + }, + resolvers: { + /** + * Looks up the user by matching their nameID to the entity name. + */ + nameIdMatchingUserEntityName(): SignInResolver { + return async (info, ctx) => { + const id = info.result.fullProfile.nameID; - return new SamlAuthProvider({ - callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, - entryPoint: config.getString('entryPoint'), - logoutUrl: config.getOptionalString('logoutUrl'), - audience: config.getOptionalString('audience'), - issuer: config.getString('issuer'), - cert: config.getString('cert'), - privateKey: config.getOptionalString('privateKey'), - authnContext: config.getOptionalStringArray('authnContext'), - identifierFormat: config.getOptionalString('identifierFormat'), - decryptionPvk: config.getOptionalString('decryptionPvk'), - signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as - | SignatureAlgorithm - | undefined, - digestAlgorithm: config.getOptionalString('digestAlgorithm'), - acceptedClockSkewMs: config.getOptionalNumber('acceptedClockSkewMs'), + if (!id) { + throw new AuthenticationError('No nameID found in SAML response'); + } - tokenIssuer, - appUrl: globalConfig.appUrl, - authHandler, - signInResolver, - logger, - catalogIdentityClient, - }); - }; -}; + return ctx.signInWithCatalogUser({ + entityRef: { name: id }, + }); + }; + }, + }, +}); + +/** + * @public + * @deprecated Use `providers.saml.create` instead + */ +export const createSamlProvider = saml.create; + +/** + * @public + * @deprecated Use `providers.saml.resolvers.nameIdMatchingUserEntityName()` instead. + */ +export const samlNameIdEntityNameSignInResolver = + saml.resolvers.nameIdMatchingUserEntityName(); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 5bd52f0c94..128dea12ef 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogApi, GetEntitiesRequest } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { BackstageIdentityResponse, @@ -26,9 +26,42 @@ import { } from '@backstage/plugin-auth-node'; import express from 'express'; import { Logger } from 'winston'; -import { TokenIssuer } from '../identity/types'; +import { TokenIssuer, TokenParams } from '../identity/types'; import { OAuthStartRequest } from '../lib/oauth/types'; import { CatalogIdentityClient } from '../lib/catalog'; +import { Entity } from '@backstage/catalog-model'; + +/** + * A query for a single user in the catalog. + * + * If `entityRef` is used, the default kind is `'User'`. + * + * If `annotations` are used, all annotations must be present and + * match the provided value exactly. Only entities of kind `'User'` will be considered. + * + * If `filter` are used they are passed on as they are to the `CatalogApi`. + * + * Regardless of the query method, the query must match exactly one entity + * in the catalog, or an error will be thrown. + * + * @public + */ +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: Exclude; + }; /** * The context that is used for auth processing. @@ -36,9 +69,36 @@ import { CatalogIdentityClient } from '../lib/catalog'; * @public */ export type AuthResolverContext = { - tokenIssuer: TokenIssuer; - catalogIdentityClient: CatalogIdentityClient; + /** @deprecated Will be removed from the context, access it via a closure instead if needed */ logger: Logger; + /** @deprecated Use the `issueToken` method instead */ + tokenIssuer: TokenIssuer; + /** @deprecated Use the `findCatalogUser` and `signInWithCatalogUser` methods instead, and the `getDefaultOwnershipEntityRefs` helper */ + catalogIdentityClient: CatalogIdentityClient; + + /** + * Issues a Backstage token using the provided parameters. + */ + issueToken(params: TokenParams): Promise<{ token: string }>; + + /** + * Finds a single user in the catalog using the provided query. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + findCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise<{ entity: Entity }>; + + /** + * Finds a single user in the catalog using the provided query, and then + * issues an identity for that user using default ownership resolution. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; }; /** @@ -54,6 +114,7 @@ export type CookieConfigurer = (ctx: { callbackUrl: string; }) => { domain: string; path: string; secure: boolean }; +/** @public */ export type AuthProviderConfig = { /** * The protocol://domain[:port] where the app is hosted. This is used to construct the @@ -143,6 +204,9 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } +/** + * @deprecated This type is deprecated and will be removed in a future release. + */ export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -154,10 +218,24 @@ export type AuthProviderFactoryOptions = { catalogApi: CatalogApi; }; -export type AuthProviderFactory = ( - options: AuthProviderFactoryOptions, -) => AuthProviderRouteHandlers; +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + logger: Logger; + resolverContext: AuthResolverContext; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + tokenManager: TokenManager; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + tokenIssuer: TokenIssuer; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + discovery: PluginEndpointDiscovery; + /** @deprecated This field has been deprecated and needs to be passed directly to the auth provider instead */ + catalogApi: CatalogApi; +}) => AuthProviderRouteHandlers; + +/** @public */ export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; @@ -245,6 +323,7 @@ export type AuthHandler = ( context: AuthResolverContext, ) => Promise; +/** @public */ export type StateEncoder = ( req: OAuthStartRequest, ) => Promise<{ encodedState: string }>; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index ec1a52ffc9..80b8a24022 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -34,6 +34,7 @@ import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import passport from 'passport'; import { Minimatch } from 'minimatch'; +import { CatalogAuthResolverContext } from '../lib/resolvers'; type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -122,6 +123,12 @@ export async function createRouter( tokenIssuer, discovery, catalogApi, + resolverContext: CatalogAuthResolverContext.create({ + logger, + catalogApi, + tokenIssuer, + tokenManager, + }), }); const r = Router(); diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index d0860296f2..5dab72f4a8 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-auth-node +## 0.2.1-next.0 + +### Patch Changes + +- 9ec4e0613e: Update to `jose` 4.6.0 +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.2.0 + +### Minor Changes + +- 15d3a3c39a: **BREAKING**: Removed the deprecated `id` and `entity` fields from `BackstageSignInResult`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.2.0-next.0 ### Minor Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index dac6c3a584..cde2a15691 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.0-next.0", + "version": "0.2.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,15 +23,16 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "jose": "^1.27.1", + "jose": "^4.6.0", "node-fetch": "^2.6.7", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", + "lodash": "^4.17.21", "msw": "^0.35.0", "uuid": "^8.0.0" }, diff --git a/plugins/auth-node/src/IdentityClient.test.ts b/plugins/auth-node/src/IdentityClient.test.ts index 88d4021dc3..5efc85cb21 100644 --- a/plugins/auth-node/src/IdentityClient.test.ts +++ b/plugins/auth-node/src/IdentityClient.test.ts @@ -15,11 +15,17 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { JSONWebKey, JWK, JWS, JWT } from 'jose'; +import { + SignJWT, + generateKeyPair, + decodeProtectedHeader, + exportJWK, +} from 'jose'; +import { cloneDeep } from 'lodash'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { v4 as uuid } from 'uuid'; import { IdentityClient } from './IdentityClient'; +import { v4 as uuid } from 'uuid'; interface AnyJWK extends Record { use: 'sig'; @@ -45,12 +51,11 @@ class FakeTokenFactory { ent?: string[]; }; }): Promise { - const key = await JWK.generate('EC', 'P-256', { - use: 'sig', - kid: uuid(), - alg: 'ES256', - }); - this.keys.push(key.toJWK(false) as unknown as AnyJWK); + const pair = await generateKeyPair('ES256'); + const publicKey = await exportJWK(pair.publicKey); + const kid = uuid(); + publicKey.kid = kid; + this.keys.push(publicKey as AnyJWK); const iss = this.options.issuer; const sub = params.claims.sub; @@ -59,10 +64,14 @@ class FakeTokenFactory { const iat = Math.floor(Date.now() / 1000); const exp = iat + this.options.keyDurationSeconds; - return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { - alg: key.alg, - kid: key.kid, - }); + return new SignJWT({ iss, sub, aud, iat, exp, ent, kid }) + .setProtectedHeader({ alg: 'ES256', ent: ent, kid: kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(pair.privateKey); } async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { @@ -71,10 +80,8 @@ class FakeTokenFactory { } function jwtKid(jwt: string): string { - const { header } = JWT.decode(jwt, { complete: true }) as { - header: { kid: string }; - }; - return header.kid; + const header = decodeProtectedHeader(jwt); + return header.kid ?? ''; } const server = setupServer(); @@ -118,13 +125,6 @@ describe('IdentityClient', () => { ); }); - it('should use the correct endpoint', async () => { - await factory.issueToken({ claims: { sub: 'foo' } }); - const keys = await factory.listPublicKeys(); - const response = await (client as any).listPublicKeys(); - expect(response).toEqual(keys); - }); - it('should throw on undefined header', async () => { return expect(async () => { await client.authenticate(undefined); @@ -144,6 +144,21 @@ describe('IdentityClient', () => { }); }); + it('should decode claims correctly', async () => { + const token = await factory.issueToken({ + claims: { sub: 'foo', ent: ['entity1', 'entity2'] }, + }); + const response = await client.authenticate(token); + expect(response).toEqual({ + token: token, + identity: { + type: 'user', + userEntityRef: 'foo', + ownershipEntityRefs: ['entity1', 'entity2'], + }, + }); + }); + it('should throw on incorrect issuer', async () => { const hackerFactory = new FakeTokenFactory({ issuer: 'hacker', @@ -196,8 +211,13 @@ describe('IdentityClient', () => { } catch (_err) { // Ignore thrown error } - // Move forward in time where the signing key has been rotated - jest.spyOn(Date, 'now').mockImplementation(() => fixedTime); + // Move forward in time where the signing key has been rotated and the + // cooldown period to look up a new public key has elapsed. + jest + .spyOn(Date, 'now') + .mockImplementation( + () => fixedTime + 30 * keyDurationSeconds * 1000 + 2, + ); const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); expect(response).toEqual({ @@ -229,36 +249,60 @@ describe('IdentityClient', () => { return await client.authenticate(fakeToken); }).rejects.toThrow(); }); - }); - describe('listPublicKeys', () => { - const defaultServiceResponse: { - keys: JSONWebKey[]; - } = { - keys: [ - { - crv: 'P-256', - x: 'JWy80Goa-8C3oaeDLnk0ANVPPMfI9T3u_T5T7W2b_ls', - y: 'Ge6jAhCDW1PFBfme2RA5ZsXN0cESiCwW29LMRPX5wkw', - kty: 'EC', - kid: 'kid-a', - alg: 'ES256', - use: 'sig', - }, - ], - }; - - beforeEach(() => { + it('should use an updated endpoint when the key is not found', async () => { + const updatedURL = 'http://backstage:9191/an-updated-base'; + const getBaseUrl = discovery.getBaseUrl; + const getExternalBaseUrl = discovery.getExternalBaseUrl; + // Generate a key and sign a token with it + await factory.issueToken({ claims: { sub: 'foo' } }); + // Only return the key from a single token + const singleKey = cloneDeep(await factory.listPublicKeys()); server.use( - rest.get(`${mockBaseUrl}/.well-known/jwks.json`, (_, res, ctx) => { - return res(ctx.json(defaultServiceResponse)); + rest.get( + `${mockBaseUrl}/.well-known/jwks.json`, + async (_, res, ctx) => { + return res(ctx.json(singleKey)); + }, + ), + ); + // Update the discovery endpoint to point to a new URL + discovery.getBaseUrl = async () => { + return updatedURL; + }; + discovery.getExternalBaseUrl = async () => { + return updatedURL; + }; + let calledUpdatedEndpoint = false; + server.use( + rest.get(`${updatedURL}/.well-known/jwks.json`, async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + calledUpdatedEndpoint = true; + return res(ctx.json(keys)); }), ); - }); - - it('should use the correct endpoint', async () => { - const response = await (client as any).listPublicKeys(); - expect(response).toEqual(defaultServiceResponse); + // Advance time + const future_11s = Date.now() + 11 * 1000; + const dateSpy = jest + .spyOn(Date, 'now') + .mockImplementation(() => future_11s); + // Issue a new token + const token = await factory.issueToken({ claims: { sub: 'foo2' } }); + const response = await client.authenticate(token); + // Verify that the endpoint was updated. + expect(calledUpdatedEndpoint).toBeTruthy(); + expect(response).toEqual({ + token: token, + identity: { + type: 'user', + userEntityRef: 'foo2', + ownershipEntityRefs: [], + }, + }); + // Restore the discovery endpoint and time + discovery.getBaseUrl = getBaseUrl; + discovery.getExternalBaseUrl = getExternalBaseUrl; + dateSpy.mockClear(); }); }); }); diff --git a/plugins/auth-node/src/IdentityClient.ts b/plugins/auth-node/src/IdentityClient.ts index 54c2670ae8..605c434431 100644 --- a/plugins/auth-node/src/IdentityClient.ts +++ b/plugins/auth-node/src/IdentityClient.ts @@ -16,8 +16,15 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthenticationError } from '@backstage/errors'; -import { JSONWebKey, JWK, JWKS, JWT } from 'jose'; -import fetch from 'node-fetch'; +import { + createRemoteJWKSet, + decodeJwt, + jwtVerify, + FlattenedJWSInput, + JWSHeaderParameters, + decodeProtectedHeader, +} from 'jose'; +import { GetKeyFunction } from 'jose/dist/types/types'; import { BackstageIdentityResponse } from './types'; const CLOCK_MARGIN_S = 10; @@ -32,8 +39,8 @@ const CLOCK_MARGIN_S = 10; export class IdentityClient { private readonly discovery: PluginEndpointDiscovery; private readonly issuer: string; - private keyStore: JWKS.KeyStore; - private keyStoreUpdated: number; + private keyStore?: GetKeyFunction; + private keyStoreUpdated: number = 0; /** * Create a new {@link IdentityClient} instance. @@ -51,8 +58,6 @@ export class IdentityClient { }) { this.discovery = options.discovery; this.issuer = options.issuer; - this.keyStore = new JWKS.KeyStore(); - this.keyStoreUpdated = 0; } /** @@ -67,22 +72,23 @@ export class IdentityClient { if (!token) { throw new AuthenticationError('No token specified'); } - // Get signing key matching token - const key = await this.getKey(token); - if (!key) { - throw new AuthenticationError('No signing key matching token found'); - } + // Verify token claims and signature // Note: Claims must match those set by TokenFactory when issuing tokens // Note: verify throws if verification fails - const decoded = JWT.IdToken.verify(token, key, { + // Check if the keystore needs to be updated + await this.refreshKeyStore(token); + if (!this.keyStore) { + throw new AuthenticationError('No keystore exists'); + } + const decoded = await jwtVerify(token, this.keyStore, { algorithms: ['ES256'], audience: 'backstage', issuer: this.issuer, - }) as { sub: string; ent: string[] }; + }); // Verified, return the matching user as BackstageIdentity // TODO: Settle internal user format/properties - if (!decoded.sub) { + if (!decoded.payload.sub) { throw new AuthenticationError('No user sub found in token'); } @@ -90,68 +96,45 @@ export class IdentityClient { token, identity: { type: 'user', - userEntityRef: decoded.sub, - ownershipEntityRefs: decoded.ent ?? [], + userEntityRef: decoded.payload.sub, + ownershipEntityRefs: decoded.payload.ent + ? (decoded.payload.ent as string[]) + : [], }, }; return user; } /** - * Returns the public signing key matching the given jwt token, - * or null if no matching key was found + * If the last keystore refresh is stale, update the keystore URL to the latest */ - private async getKey(rawJwtToken: string): Promise { - const { header, payload } = JWT.decode(rawJwtToken, { - complete: true, - }) as { - header: { kid: string }; - payload: { iat: number }; - }; + private async refreshKeyStore(rawJwtToken: string): Promise { + const payload = await decodeJwt(rawJwtToken); + const header = await decodeProtectedHeader(rawJwtToken); // Refresh public keys if needed + let keyStoreHasKey; + try { + if (this.keyStore) { + // Check if the key is present in the keystore + const [_, rawPayload, rawSignature] = rawJwtToken.split('.'); + keyStoreHasKey = await this.keyStore(header, { + payload: rawPayload, + signature: rawSignature, + }); + } + } catch (error) { + keyStoreHasKey = false; + } + // Refresh public key URL if needed // Add a small margin in case clocks are out of sync - const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid }); const issuedAfterLastRefresh = payload?.iat && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S; if (!keyStoreHasKey && issuedAfterLastRefresh) { - await this.refreshKeyStore(); + const url = await this.discovery.getBaseUrl('auth'); + const endpoint = new URL(`${url}/.well-known/jwks.json`); + this.keyStore = createRemoteJWKSet(endpoint); + this.keyStoreUpdated = Date.now() / 1000; } - - return this.keyStore.get({ kid: header.kid }); - } - - /** - * Lists public part of keys used to sign Backstage Identity tokens - */ - private async listPublicKeys(): Promise<{ - keys: JSONWebKey[]; - }> { - const url = `${await this.discovery.getBaseUrl( - 'auth', - )}/.well-known/jwks.json`; - const response = await fetch(url); - - if (!response.ok) { - const payload = await response.text(); - const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`; - throw new Error(message); - } - - const publicKeys: { keys: JSONWebKey[] } = await response.json(); - - return publicKeys; - } - - /** - * Fetches public keys and caches them locally - */ - private async refreshKeyStore(): Promise { - const now = Date.now() / 1000; - const publicKeys = await this.listPublicKeys(); - this.keyStore = JWKS.asKeyStore({ - keys: publicKeys.keys.map(key => key as JSONWebKey), - }); - this.keyStoreUpdated = now; } } diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index c8184ace2a..4c839ac65e 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-azure-devops-backend +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.3.10 + +### Patch Changes + +- 236245d9f8: Stop loading all teams when plugin is initialized +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 7a19af6617..64e671a109 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.10-next.1", + "version": "0.3.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/plugin-azure-devops-common": "^0.2.2", "@types/express": "^4.17.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index eadce6bb88..372d9cc0bb 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 9b7c615875..8dcb89bb4b 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-azure-devops +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.20 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.20-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.20-next.0 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 4811233405..534dff7267 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.20-next.0", + "version": "0.1.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/plugin-azure-devops-common": "^0.2.2", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index fee6096dd0..a13e31105a 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-badges-backend +## 0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.1.25 + +### Patch Changes + +- 4c93fd36ee: allow overriding `DefaultBadgeBuilder.getMarkdownCode` +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + ## 0.1.25-next.1 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index caadaa460e..6a2c2373a0 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.25-next.1", + "version": "0.1.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@types/express": "^4.17.6", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 90fd5eb8f2..0b6017431f 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-badges +## 0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.2.28 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.2.28-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.2.28-next.0 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 0e69b34824..80e457626d 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.28-next.0", + "version": "0.2.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 25f49f5800..b00a693709 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-bazaar-backend +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/backend-test-utils@0.1.24-next.0 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + - @backstage/backend-test-utils@0.1.23 + ## 0.1.15-next.0 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 15b7596ef1..642aed27de 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.15-next.0", + "version": "0.1.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/backend-test-utils": "^0.1.23-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-test-utils": "^0.1.24-next.0", "@backstage/config": "^1.0.0", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index e859f1f1ec..910b5a1c4d 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-bazaar +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.17.1-next.0 + - @backstage/plugin-catalog@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.19 + +### Patch Changes + +- 69738379c7: Pass authorization header with Backstage token to backend requests. +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/cli@0.17.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-catalog@1.1.0 + - @backstage/catalog-client@1.0.1 + +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.17.0-next.3 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-catalog@1.1.0-next.3 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 417d5d0ba9..4146fe51c5 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.19-next.1", + "version": "0.1.20-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/cli": "^0.17.0-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog": "^1.1.0-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/dev-utils": "^1.0.1-next.0", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.1.5" }, diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index db71faef2f..88c9159f06 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-bitrise +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.31-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.31-next.0 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 4dbd5c3cae..7f330e9afb 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.31-next.0", + "version": "0.1.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index dc96fa35fa..af45cafdca 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,190 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.5-next.0 + +### Patch Changes + +- bffec1c96a: Fix S3 object URL creation at AwsS3EntityProvider by + + - handle absence of region config, + - handle regions with region-less URIs (us-east-1), + - apply URI encoding, + - and simplify the logic overall. + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## 0.1.4 + +### Patch Changes + +- 5969c4b65c: Add a new provider `AwsS3EntityProvider` as replacement for `AwsS3DiscoveryProcessor`. + + In order to migrate from the `AwsS3DiscoveryProcessor` you need to apply + the following changes: + + **Before:** + + ```yaml + # app-config.yaml + + catalog: + locations: + - type: s3-discovery + target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/ + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors ... */ + builder.addProcessor(new AwsS3DiscoveryProcessor(env.reader)); + ``` + + **After:** + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + yourProviderId: # identifies your dataset / provider independent of config changes + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors and/or providers ... */ + builder.addEntityProvider( + ...AwsS3EntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), + ); + ``` + + For simple setups, you can omit the provider ID at the config + which has the same effect as using `default` for it. + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + # uses "default" as provider ID + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + +- 776a91ea3a: Corrected title and URL to setup documentation in README +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## 0.1.4-next.2 + +### Patch Changes + +- 5969c4b65c: Add a new provider `AwsS3EntityProvider` as replacement for `AwsS3DiscoveryProcessor`. + + In order to migrate from the `AwsS3DiscoveryProcessor` you need to apply + the following changes: + + **Before:** + + ```yaml + # app-config.yaml + + catalog: + locations: + - type: s3-discovery + target: https://sample-bucket.s3.us-east-2.amazonaws.com/prefix/ + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors ... */ + builder.addProcessor(new AwsS3DiscoveryProcessor(env.reader)); + ``` + + **After:** + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + yourProviderId: # identifies your dataset / provider independent of config changes + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + + ```ts + /* packages/backend/src/plugins/catalog.ts */ + + import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; + + const builder = await CatalogBuilder.create(env); + /** ... other processors and/or providers ... */ + builder.addEntityProvider( + ...AwsS3EntityProvider.fromConfig(env.config, { + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 30 }), + timeout: Duration.fromObject({ minutes: 3 }), + }), + }), + ); + ``` + + For simple setups, you can omit the provider ID at the config + which has the same effect as using `default` for it. + + ```yaml + # app-config.yaml + + catalog: + providers: + awsS3: + # uses "default" as provider ID + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + ``` + +- 776a91ea3a: Corrected title and URL to setup documentation in README +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0-next.3 + - @backstage/backend-common@0.13.2-next.2 + - @backstage/integration@1.1.0-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index a115efeacb..26947b8c50 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.4-next.1", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,13 +33,13 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/backend-tasks": "^0.3.0-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "aws-sdk": "^2.840.0", "lodash": "^4.17.21", @@ -48,7 +48,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151", "aws-sdk-mock": "^5.2.1", "yaml": "^1.9.2" diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts index 5e5901a770..b186b3d1b8 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts @@ -38,24 +38,8 @@ class PersistingTaskRunner implements TaskRunner { const logger = getVoidLogger(); describe('AwsS3EntityProvider', () => { - const config = new ConfigReader({ - catalog: { - providers: { - awsS3: { - anyProviderId: { - bucketName: 'bucket-1', - region: 'us-east-1', - prefix: 'sub/dir/', - }, - }, - }, - }, - }); - - const schedule = new PersistingTaskRunner(); - AWSMock.setSDKInstance(aws); - const createObjectList = (...keys: string[]): aws.S3.ObjectList => { + const createObjectList = (keys: string[]): aws.S3.ObjectList => { const objects = keys.map(key => { return { Key: key, @@ -65,24 +49,55 @@ describe('AwsS3EntityProvider', () => { return objects as aws.S3.ObjectList; }; + const keys = ['key1.yaml', 'key2.yaml', 'key3.yaml', 'key 4.yaml']; + AWSMock.mock('S3', 'listObjectsV2', async req => { const prefix = req.Prefix ?? ''; if (!req.ContinuationToken) { return { - Contents: createObjectList(`${prefix}key1.yaml`, `${prefix}key2.yaml`), + Contents: createObjectList( + keys + .slice(0, Math.ceil(keys.length / 2)) + .map(key => `${prefix}${key}`), + ), NextContinuationToken: 'next-token', } as aws.S3.Types.ListObjectsV2Output; } return { - Contents: createObjectList(`${prefix}key3.yaml`, `${prefix}key4.yaml`), + Contents: createObjectList( + keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`), + ), } as aws.S3.Types.ListObjectsV2Output; }); - afterEach(() => jest.resetAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + process.env.AWS_REGION = undefined; + }); - it('apply full update on scheduled execution', async () => { + const expectMutation = async ( + providerId: string, + providerConfig: object, + expectedBaseUrl: string, + names: Record, + integrationConfig?: object, + ) => { + const config = new ConfigReader({ + integrations: { + awsS3: integrationConfig ? [integrationConfig] : [], + }, + catalog: { + providers: { + awsS3: { + [providerId]: providerConfig, + }, + }, + }, + }); + + const schedule = new PersistingTaskRunner(); const entityProviderConnection: EntityProviderConnection = { applyMutation: jest.fn(), }; @@ -91,106 +106,121 @@ describe('AwsS3EntityProvider', () => { logger, schedule, })[0]; - expect(provider.getProviderName()).toEqual('awsS3-provider:anyProviderId'); + expect(provider.getProviderName()).toEqual(`awsS3-provider:${providerId}`); await provider.connect(entityProviderConnection); const taskDef = schedule.getTasks()[0]; - expect(taskDef.id).toEqual('awsS3-provider:anyProviderId:refresh'); + expect(taskDef.id).toEqual(`awsS3-provider:${providerId}:refresh`); await (taskDef.fn as () => Promise)(); + const expectedEntities = keys.map(key => { + const url = encodeURI(`${expectedBaseUrl}${key}`); + return { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: names[key], + }, + spec: { + presence: 'required', + target: `${url}`, + type: 'url', + }, + }, + locationKey: `awsS3-provider:${providerId}`, + }; + }); + expect(entityProviderConnection.applyMutation).toBeCalledWith({ type: 'full', - entities: [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml', - 'backstage.io/managed-by-origin-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml', - }, - name: 'generated-980e6ad47fbfbfeead708a9c7c87331b7540296a', - }, - spec: { - presence: 'required', - target: - 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key1.yaml', - type: 'url', - }, - }, - locationKey: 'awsS3-provider:anyProviderId', - }, - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml', - 'backstage.io/managed-by-origin-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml', - }, - name: 'generated-266794d8e789089dddba2b42cd79e70b149aa61c', - }, - spec: { - presence: 'required', - target: - 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key2.yaml', - type: 'url', - }, - }, - locationKey: 'awsS3-provider:anyProviderId', - }, - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml', - 'backstage.io/managed-by-origin-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml', - }, - name: 'generated-96f0cdcd7e33aa687c19d160ec7d5b1975cb9ea1', - }, - spec: { - presence: 'required', - target: - 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key3.yaml', - type: 'url', - }, - }, - locationKey: 'awsS3-provider:anyProviderId', - }, - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml', - 'backstage.io/managed-by-origin-location': - 'url:https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml', - }, - name: 'generated-cd1a799b5ecfc055a0c672654420af3afeb648d3', - }, - spec: { - presence: 'required', - target: - 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/key4.yaml', - type: 'url', - }, - }, - locationKey: 'awsS3-provider:anyProviderId', - }, - ], + entities: expectedEntities, }); + }; + + // eslint-disable-next-line jest/expect-expect + it('apply full update on scheduled execution', async () => { + return expectMutation( + 'regionalStatic', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + region: 'eu-west-1', + }, + 'https://s3.eu-west-1.amazonaws.com/bucket-1/sub/dir/', + { + 'key1.yaml': 'generated-7f6d5861b0b3401a38b5fe62e6c7ca11da5fd6d8', + 'key2.yaml': 'generated-a290be145586042af7d80715626399c9d661718d', + 'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d', + 'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('us-east-1 has region-less URLs', async () => { + return expectMutation( + 'usEast1', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + region: 'us-east-1', + }, + 'https://s3.amazonaws.com/bucket-1/sub/dir/', + { + 'key1.yaml': 'generated-f7e3f1c89f62cbf0d82db16452faaa7c040fc331', + 'key2.yaml': 'generated-925173f7dba1acaa73cac5ef4d10c3ed7660aa25', + 'key3.yaml': 'generated-d94cf017911ed7fb3be4a62a3dae1f5202879de3', + 'key 4.yaml': 'generated-f917fca0cfacc2be478ca2a3cff94d00917a1cde', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('fallback region if absent', async () => { + // logic will use region information provided from + // profile < AWS_REGION < argument to client/command + process.env.AWS_REGION = 'eu-central-1'; + + return expectMutation( + 'absentRegion', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + }, + 'https://s3.eu-central-1.amazonaws.com/bucket-1/sub/dir/', + { + 'key1.yaml': 'generated-285d144b5c1e24e801b97f61f794d566dabc7236', + 'key2.yaml': 'generated-36b78d7fa69690059797d4cc4f40c2a5eaed1d6d', + 'key3.yaml': 'generated-a39ad804bf65993cbc0f3810ff128a3c91daf768', + 'key 4.yaml': 'generated-bd7622ac86c313cb812d90e335b2f5f6bff14670', + }, + ); + }); + + // eslint-disable-next-line jest/expect-expect + it('custom endpoint', async () => { + return expectMutation( + 'customEndpoint', + { + bucketName: 'bucket-1', + prefix: 'sub/dir/', + }, + 'http://localhost:1234/bucket-1/sub/dir/', + { + 'key1.yaml': 'generated-e1f1dcfe44967b899a49d856cadcfa1ffc72a9f6', + 'key2.yaml': 'generated-6b09503942fe41566339f2eccfbb3380a022494b', + 'key3.yaml': 'generated-17274cdefac2feb3702f41605ca48aa4370d20a9', + 'key 4.yaml': 'generated-659f17a2429f107db04850f9081e3e9c56084aeb', + }, + { + endpoint: 'http://localhost:1234', + }, + ); }); }); diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index 2e64699d8b..cee5eea1a4 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -57,7 +57,7 @@ export class AwsS3EntityProvider implements EntityProvider { // Even though the awsS3 integration allows a config array // there is no *real* support for multiple configs. // Usually, there will be just the integration for the default host. - // In case, a config custom endpoint is used, the host from this endpoint + // In case, a custom endpoint is used, the host from this endpoint // will be extracted and used as host (e.g., localhost when used with LocalStack) // and the default integration will be added as second integration. // In this case, we still want the first one though, but have no means to select it @@ -80,7 +80,7 @@ export class AwsS3EntityProvider implements EntityProvider { private constructor( private readonly config: AwsS3Config, - private readonly integration: AwsS3Integration, + integration: AwsS3Integration, logger: Logger, schedule: TaskRunner, ) { @@ -196,16 +196,8 @@ export class AwsS3EntityProvider implements EntityProvider { private createObjectUrl(key: string): string { const bucketName = this.config.bucketName; - const endpoint = this.integration.config.endpoint; + const endpoint = this.s3.endpoint.href; - if (endpoint) { - if (endpoint.startsWith(`https://${bucketName}.`)) { - return `${endpoint}/${key}`; - } - - return `${endpoint}/${bucketName}/${key}`; - } - - return `https://${bucketName}.s3.${this.config.region}.amazonaws.com/${key}`; + return encodeURI(`${endpoint}${bucketName}/${key}`); } } diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 7846323524..bbc54de859 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 076d35babf..b0482f6b5c 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.2-next.1", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "lodash": "^4.17.21", "msw": "^0.35.0", @@ -46,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index 7a42da0806..d4e1d5cc17 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## 0.1.2 + +### Patch Changes + +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- 9fed130139: split BitbucketClient into BitbucketCloudClient, BitbucketServerClient +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + ## 0.1.2-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 12ea1a3ae5..89657ceb68 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.1.2-next.2", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.2", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "lodash": "^4.17.21", "msw": "^0.35.0", @@ -46,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.2", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-gerrit/.eslintrc.js b/plugins/catalog-backend-module-gerrit/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md new file mode 100644 index 0000000000..e226a5e65a --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -0,0 +1,16 @@ +# @backstage/plugin-catalog-backend-module-gerrit + +## 0.1.0-next.0 + +### Minor Changes + +- 566407bf8a: Initial version of the `plugin-catalog-backend-module-gerrit` plugin + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 diff --git a/plugins/catalog-backend-module-gerrit/README.md b/plugins/catalog-backend-module-gerrit/README.md new file mode 100644 index 0000000000..8b0e0f0c69 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for Gerrit + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at Gerrit integrations. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/gerrit/discovery.md) +for details on how to install and configure the plugin. diff --git a/plugins/catalog-backend-module-gerrit/api-report.md b/plugins/catalog-backend-module-gerrit/api-report.md new file mode 100644 index 0000000000..7eae94c2a8 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gerrit" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { TaskRunner } from '@backstage/backend-tasks'; + +// @public (undocumented) +export class GerritEntityProvider implements EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): GerritEntityProvider[]; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(logger: Logger): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-backend-module-gerrit/config.d.ts b/plugins/catalog-backend-module-gerrit/config.d.ts new file mode 100644 index 0000000000..14e8f58d8f --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/config.d.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +interface GerritConfig { + /** + * (Required) The host of the Gerrit integration to use. + * @visibility backend + */ + host: string; + /** + * (Required) The query to use for the "List Projects" API call. Used to limit the + * scope of the projects that the provider tries to ingest. + * @visibility backend + */ + query: string; + /** + * (Optional) Branch. + * The branch where the provider will try to find entities. Defaults to "master". + * @visibility backend + */ + branch?: string; +} + +export interface Config { + catalog?: { + /** + * List of provider-specific options and attributes + */ + providers?: { + /** + * GerritEntityProvider configuration + * + * Maps provider id with configuration. + */ + gerrit?: Record; + }; + }; +} diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json new file mode 100644 index 0000000000..75a7a17aa2 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -0,0 +1,54 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-gerrit", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-gerrit" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/config": "^1.0.0", + "@backstage/errors": "^1.0.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", + "fs-extra": "10.1.0", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "uuid": "^8.0.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", + "@types/fs-extra": "^9.0.1" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/catalog-backend-module-gerrit/src/index.ts b/plugins/catalog-backend-module-gerrit/src/index.ts new file mode 100644 index 0000000000..133772d4b1 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { GerritEntityProvider } from './providers/GerritEntityProvider'; diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts new file mode 100644 index 0000000000..ee65d536c7 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts @@ -0,0 +1,206 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { rest } from 'msw'; +import fs from 'fs-extra'; +import path from 'path'; +import { setupServer } from 'msw/node'; +import { GerritEntityProvider } from './GerritEntityProvider'; + +const server = setupServer(); + +const getJsonFixture = (fileName: string) => + JSON.parse( + fs.readFileSync( + path.resolve(__dirname, `__fixtures__/${fileName}`), + 'utf8', + ), + ); + +class PersistingTaskRunner implements TaskRunner { + private tasks: TaskInvocationDefinition[] = []; + + getTasks() { + return this.tasks; + } + + run(task: TaskInvocationDefinition): Promise { + this.tasks.push(task); + return Promise.resolve(undefined); + } +} + +const logger = getVoidLogger(); + +describe('GerritEntityProvider', () => { + beforeAll(() => server.listen()); + afterEach(() => { + jest.resetAllMocks(); + server.resetHandlers(); + }); + afterAll(() => server.close()); + + const config = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-training': { + host: 'g.com', + query: 'state=ACTIVE&prefix=training', + branch: 'main', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'g.com', + baseUrl: 'https://g.com/gerrit', + gitilesBaseUrl: 'https:/g.com/gitiles', + }, + ], + }, + }); + const schedule = new PersistingTaskRunner(); + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + }; + + it('discovers projects from the api.', async () => { + const repoBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/listProjectsBody.txt'), + ); + const expected = getJsonFixture('expectedProviderEntities.json'); + + server.use( + rest.get('https://g.com/gerrit/projects/', (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.body(repoBuffer), + ), + ), + ); + + const provider = GerritEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'gerrit-provider:active-training', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual('gerrit-provider:active-training:refresh'); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toBeCalledWith(expected); + }); + + it('handles api errors.', async () => { + const provider = GerritEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + server.use( + rest.get('https://g.com/gerrit/projects/', (_, res, ctx) => + res(ctx.status(500, 'Error!.')), + ), + ); + + await provider.connect(entityProviderConnection); + const taskDef = schedule.getTasks()[0]; + + await (taskDef.fn as () => Promise)(); + expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled(); + }); + + it('can create multiple providers from config.', async () => { + const configTwoProviders = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-g1': { + host: 'gerrit1.com', + query: 'state=ACTIVE', + }, + 'active-g2': { + host: 'gerrit2.com', + query: 'state=ACTIVE', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'gerrit1.com', + }, + { + host: 'gerrit2.com', + }, + ], + }, + }); + + const providers = GerritEntityProvider.fromConfig(configTwoProviders, { + logger, + schedule, + }); + expect(providers).toHaveLength(2); + expect(providers[0]).toBeInstanceOf(GerritEntityProvider); + expect(providers[0].getProviderName()).toEqual('gerrit-provider:active-g1'); + expect(providers[1]).toBeInstanceOf(GerritEntityProvider); + expect(providers[1].getProviderName()).toEqual('gerrit-provider:active-g2'); + }); + + it('throws if integration is missing.', () => { + const configMissingIntegration = new ConfigReader({ + catalog: { + providers: { + gerrit: { + 'active-g2': { + host: 'gerrit2.com', + query: 'state=ACTIVE', + }, + }, + }, + }, + integrations: { + gerrit: [ + { + host: 'gerrit1.com', + }, + ], + }, + }); + + expect(() => + GerritEntityProvider.fromConfig(configMissingIntegration, { + logger, + schedule, + }), + ).toThrow(/No gerrit integration/); + }); +}); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts new file mode 100644 index 0000000000..beb4052073 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts @@ -0,0 +1,165 @@ +/* + * 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 { TaskRunner } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { + EntityProvider, + EntityProviderConnection, + LocationSpec, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-backend'; +import fetch, { Response } from 'node-fetch'; +import { + GerritIntegration, + getGerritProjectsApiUrl, + getGerritRequestOptions, + parseGerritJsonResponse, + ScmIntegrations, +} from '@backstage/integration'; +import * as uuid from 'uuid'; +import { Logger } from 'winston'; + +import { readGerritConfigs } from './config'; +import { GerritProjectQueryResult, GerritProviderConfig } from './types'; + +/** @public */ +export class GerritEntityProvider implements EntityProvider { + private readonly config: GerritProviderConfig; + private readonly integration: GerritIntegration; + private readonly logger: Logger; + private readonly scheduleFn: () => Promise; + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + logger: Logger; + schedule: TaskRunner; + }, + ): GerritEntityProvider[] { + const providerConfigs = readGerritConfigs(configRoot); + const integrations = ScmIntegrations.fromConfig(configRoot).gerrit; + const providers: GerritEntityProvider[] = []; + + providerConfigs.forEach(providerConfig => { + const integration = integrations.byHost(providerConfig.host); + if (!integration) { + throw new InputError( + `No gerrit integration found that matches host ${providerConfig.host}`, + ); + } + providers.push( + new GerritEntityProvider( + providerConfig, + integration, + options.logger, + options.schedule, + ), + ); + }); + return providers; + } + + private constructor( + config: GerritProviderConfig, + integration: GerritIntegration, + logger: Logger, + schedule: TaskRunner, + ) { + this.config = config; + this.integration = integration; + this.logger = logger.child({ + target: this.getProviderName(), + }); + this.scheduleFn = this.createScheduleFn(schedule); + } + + getProviderName(): string { + return `gerrit-provider:${this.config.id}`; + } + + async connect(connection: EntityProviderConnection): Promise { + this.connection = connection; + await this.scheduleFn(); + } + + private createScheduleFn(schedule: TaskRunner): () => Promise { + return async () => { + const taskId = `${this.getProviderName()}:refresh`; + return schedule.run({ + id: taskId, + fn: async () => { + const logger = this.logger.child({ + class: GerritEntityProvider.prototype.constructor.name, + taskId, + taskInstanceId: uuid.v4(), + }); + + try { + await this.refresh(logger); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } + + async refresh(logger: Logger): Promise { + if (!this.connection) { + throw new Error('Gerrit discovery connection not initialized'); + } + + let response: Response; + + const baseProjectApiUrl = getGerritProjectsApiUrl(this.integration.config); + const projectQueryUrl = `${baseProjectApiUrl}?${this.config.query}`; + try { + response = await fetch(projectQueryUrl, { + method: 'GET', + ...getGerritRequestOptions(this.integration.config), + }); + } catch (e) { + throw new Error( + `Failed to list Gerrit projects for query ${this.config.query}, ${e}`, + ); + } + const gerritProjectsResponse = (await parseGerritJsonResponse( + response as any, + )) as GerritProjectQueryResult; + const projects = Object.keys(gerritProjectsResponse); + + const locations = projects.map(project => this.createLocationSpec(project)); + await this.connection.applyMutation({ + type: 'full', + entities: locations.map(location => ({ + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + })), + }); + logger.info(`Found ${locations.length} locations.`); + } + + private createLocationSpec(project: string): LocationSpec { + return { + type: 'url', + target: `${this.integration.config.gitilesBaseUrl}/${project}/+/refs/heads/${this.config.branch}/catalog-info.yaml`, + presence: 'optional', + }; + } +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json new file mode 100644 index 0000000000..f7f707fd1a --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/expectedProviderEntities.json @@ -0,0 +1,43 @@ +{ + "entities": [ + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Location", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "url:https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml", + "backstage.io/managed-by-origin-location": "url:https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml" + }, + "name": "generated-acbbddb9193be55b61de6a7c1d39b3dcaaf27525" + }, + "spec": { + "presence": "optional", + "target": "https:/g.com/gitiles/training/gerrit/+/refs/heads/main/catalog-info.yaml", + "type": "url" + } + }, + "locationKey": "gerrit-provider:active-training" + }, + { + "entity": { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Location", + "metadata": { + "annotations": { + "backstage.io/managed-by-location": "url:https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml", + "backstage.io/managed-by-origin-location": "url:https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml" + }, + "name": "generated-4b8debb1c2ffa8d58c0bdf3d75abcf2252494e65" + }, + "spec": { + "presence": "optional", + "target": "https:/g.com/gitiles/training/sample/+/refs/heads/main/catalog-info.yaml", + "type": "url" + } + }, + "locationKey": "gerrit-provider:active-training" + } + ], + "type": "full" +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt new file mode 100644 index 0000000000..4f7054027c --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/__fixtures__/listProjectsBody.txt @@ -0,0 +1,2 @@ +)]}' +{"training/gerrit":{"id":"training%2Fgerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"","target":"_blank"}]},"training/sample":{"id":"training%2Fsample","state":"ACTIVE","web_links":[{"name":"browse","url":"","target":"_blank"}]}} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts new file mode 100644 index 0000000000..0422b4e4ea --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { readGerritConfigs } from './config'; + +describe('readGerritConfigs', () => { + it('reads all provider configs', () => { + const provider1 = { + host: 'gerrit1.com', + query: 'state=ACTIVE', + branch: 'main', + }; + const provider2 = { + host: 'gerrit2.com', + query: 'state=ACTIVE', + branch: 'main', + }; + const config = { + catalog: { + providers: { + gerrit: { + 'active-g1': provider1, + 'active-g2': provider2, + }, + }, + }, + }; + + const actual = readGerritConfigs(new ConfigReader(config)); + + expect(actual).toHaveLength(2); + expect(actual[0]).toEqual({ ...provider1, id: 'active-g1' }); + expect(actual[1]).toEqual({ ...provider2, id: 'active-g2' }); + }); + it('provides default values', () => { + const provider = { + host: 'gerrit1.com', + query: 'state=ACTIVE', + }; + const config = { + catalog: { + providers: { + gerrit: { + 'active-g1': provider, + }, + }, + }, + }; + const actual = readGerritConfigs(new ConfigReader(config)); + expect(actual).toHaveLength(1); + expect(actual[0]).toEqual({ + branch: 'master', + id: 'active-g1', + ...provider, + }); + }); +}); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.ts new file mode 100644 index 0000000000..7a28ddd8ea --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.ts @@ -0,0 +1,47 @@ +/* + * 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 { Config } from '@backstage/config'; +import { GerritProviderConfig } from './types'; + +function readGerritConfig(id: string, config: Config): GerritProviderConfig { + const branch = config.getOptionalString('branch') ?? 'master'; + const host = config.getString('host'); + const query = config.getString('query'); + + return { + branch, + host, + id, + query, + }; +} + +export function readGerritConfigs(config: Config): GerritProviderConfig[] { + const configs: GerritProviderConfig[] = []; + + const providerConfigs = config.getOptionalConfig('catalog.providers.gerrit'); + + if (!providerConfigs) { + return configs; + } + + for (const id of providerConfigs.keys()) { + configs.push(readGerritConfig(id, providerConfigs.getConfig(id))); + } + + return configs; +} diff --git a/plugins/catalog-backend-module-gerrit/src/providers/types.ts b/plugins/catalog-backend-module-gerrit/src/providers/types.ts new file mode 100644 index 0000000000..1e96bd1ee4 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/providers/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type GerritProjectInfo = { + id: string; + name: string; + parent?: string; + state?: string; +}; + +export type GerritProjectQueryResult = Record; + +export type GerritProviderConfig = { + host: string; + query: string; + id: string; + branch?: string; +}; diff --git a/plugins/catalog-backend-module-gerrit/src/setupTests.ts b/plugins/catalog-backend-module-gerrit/src/setupTests.ts new file mode 100644 index 0000000000..8b9b6bd586 --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 7b143c6426..d377e95d42 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-github +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index ed0b967942..6069834cbc 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.1.2-next.1", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "@octokit/graphql": "^4.5.8", "lodash": "^4.17.21", @@ -47,8 +47,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index ad38bd6963..43aa9be970 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index f0254d7e99..7e550d0a2f 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.1.2-next.1", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "lodash": "^4.17.21", "msw": "^0.35.0", @@ -46,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index ad30b085a7..795219a005 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## 0.4.2 + +### Patch Changes + +- ebc0aef2c6: Changed relationship creation in LDAP data processor to leverage entityRefs instead of metadata.name +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + ## 0.4.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 41b9378a83..6d763662de 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.4.2-next.1", + "version": "0.4.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-tasks": "^0.3.0-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@backstage/types": "^1.0.0", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -46,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-ldap/src/ldap/org.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/org.test.ts index 254f511c4f..c675ee06b5 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/org.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/org.test.ts @@ -19,66 +19,80 @@ import { buildMemberOf, buildOrgHierarchy } from './org'; function g( name: string, + namespace: string, parent: string | undefined, children: string[], ): GroupEntity { return { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', - metadata: { name }, + metadata: { name, namespace }, spec: { type: 'team', parent, children }, }; } describe('buildOrgHierarchy', () => { it('adds groups to their parent.children', () => { - const a = g('a', undefined, []); - const b = g('b', 'a', []); - const c = g('c', 'b', []); - const d = g('d', 'a', []); + const a = g('a', 'a-namespace', undefined, []); + const b = g('b', 'b-namespace', 'group:a-namespace/a', []); + const c = g('c', 'c-namespace', 'group:b-namespace/b', []); + const d = g('d', 'd-namespace', 'group:a-namespace/a', []); buildOrgHierarchy([a, b, c, d]); - expect(a.spec.children).toEqual(expect.arrayContaining(['b', 'd'])); - expect(b.spec.children).toEqual(expect.arrayContaining(['c'])); + expect(a.spec.children).toEqual( + expect.arrayContaining(['group:b-namespace/b', 'group:d-namespace/d']), + ); + expect(b.spec.children).toEqual( + expect.arrayContaining(['group:c-namespace/c']), + ); expect(c.spec.children).toEqual([]); expect(d.spec.children).toEqual([]); }); it('sets parent of groups children', () => { - const a = g('a', undefined, ['b', 'd']); - const b = g('b', undefined, ['c']); - const c = g('c', undefined, []); - const d = g('d', undefined, []); + const a = g('a', 'a-namespace', undefined, [ + 'group:b-namespace/b', + 'group:d-namespace/d', + ]); + const b = g('b', 'b-namespace', undefined, ['group:c-namespace/c']); + const c = g('c', 'c-namespace', undefined, []); + const d = g('d', 'd-namespace', undefined, []); buildOrgHierarchy([a, b, c, d]); expect(a.spec.parent).toBeUndefined(); - expect(b.spec.parent).toBe('a'); - expect(c.spec.parent).toBe('b'); - expect(d.spec.parent).toBe('a'); + expect(b.spec.parent).toBe('group:a-namespace/a'); + expect(c.spec.parent).toBe('group:b-namespace/b'); + expect(d.spec.parent).toBe('group:a-namespace/a'); }); }); describe('buildMemberOf', () => { it('fills indirect member of groups', () => { - const a = g('a', undefined, []); - const b = g('b', 'a', []); - const c = g('c', 'b', []); + const a = g('a', 'a-namespace', undefined, []); + const b = g('b', 'b-namespace', 'group:a-namespace/a', []); + const c = g('c', 'c-namespace', 'group:b-namespace/b', []); const u: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { name: 'n' }, - spec: { profile: {}, memberOf: ['c'] }, + spec: { profile: {}, memberOf: ['group:c-namespace/c'] }, }; const groups = [a, b, c]; buildOrgHierarchy(groups); buildMemberOf(groups, [u]); - expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + expect(u.spec.memberOf).toEqual( + expect.arrayContaining([ + 'group:a-namespace/a', + 'group:b-namespace/b', + 'group:c-namespace/c', + ]), + ); }); it('takes group spec.members into account', () => { - const a = g('a', undefined, []); - const b = g('b', 'a', []); - const c = g('c', 'b', []); - c.spec.members = ['n']; + const a = g('a', 'a-namespace', undefined, []); + const b = g('b', 'b-namespace', 'group:a-namespace/a', []); + const c = g('c', 'c-namespace', 'group:b-namespace/b', []); + c.spec.members = ['user:default/n']; const u: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', @@ -89,6 +103,36 @@ describe('buildMemberOf', () => { const groups = [a, b, c]; buildOrgHierarchy(groups); buildMemberOf(groups, [u]); - expect(u.spec.memberOf).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + expect(u.spec.memberOf).toEqual( + expect.arrayContaining([ + 'group:a-namespace/a', + 'group:b-namespace/b', + 'group:c-namespace/c', + ]), + ); + }); + + it('is not affected by `:` or `/` in the group or user name', () => { + const a = g('a:a/a', 'a-namespace', undefined, []); + const b = g('b:b/b', 'b-namespace', 'group:a-namespace/a:a/a', []); + const c = g('c:c/c', 'c-namespace', 'group:b-namespace/b:b/b', []); + c.spec.members = ['user:default/n:n/n']; + const u: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n:n/n' }, + spec: { profile: {}, memberOf: [] }, + }; + + const groups = [a, b, c]; + buildOrgHierarchy(groups); + buildMemberOf(groups, [u]); + expect(u.spec.memberOf).toEqual( + expect.arrayContaining([ + 'group:a-namespace/a:a/a', + 'group:b-namespace/b:b/b', + 'group:c-namespace/c:c/c', + ]), + ); }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/org.ts b/plugins/catalog-backend-module-ldap/src/ldap/org.ts index 28ccbd1d3b..8f657b6625 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/org.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/org.ts @@ -14,25 +14,29 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + GroupEntity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; // TODO: Copied from plugin-catalog-backend, but we could also export them from // there. Or move them to catalog-model. export function buildOrgHierarchy(groups: GroupEntity[]) { - const groupsByName = new Map(groups.map(g => [g.metadata.name, g])); + const groupsByRef = new Map(groups.map(g => [stringifyEntityRef(g), g])); // // Make sure that g.parent.children contain g // for (const group of groups) { - const selfName = group.metadata.name; - const parentName = group.spec.parent; - if (parentName) { - const parent = groupsByName.get(parentName); - if (parent && !parent.spec.children.includes(selfName)) { - parent.spec.children.push(selfName); + const selfRef = stringifyEntityRef(group); + const parentRef = group.spec.parent; + if (parentRef) { + const parent = groupsByRef.get(parentRef); + if (parent && !parent.spec.children.includes(selfRef)) { + parent.spec.children.push(selfRef); } } } @@ -42,11 +46,11 @@ export function buildOrgHierarchy(groups: GroupEntity[]) { // for (const group of groups) { - const selfName = group.metadata.name; - for (const childName of group.spec.children) { - const child = groupsByName.get(childName); + const selfRef = stringifyEntityRef(group); + for (const childRef of group.spec.children) { + const child = groupsByRef.get(childRef); if (child && !child.spec.parent) { - child.spec.parent = selfName; + child.spec.parent = selfRef; } } } @@ -55,7 +59,7 @@ export function buildOrgHierarchy(groups: GroupEntity[]) { // Ensure that users have their transitive group memberships. Requires that // the groups were previously processed with buildOrgHierarchy() export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) { - const groupsByName = new Map(groups.map(g => [g.metadata.name, g])); + const groupsByRef = new Map(groups.map(g => [stringifyEntityRef(g), g])); users.forEach(user => { const transitiveMemberOf = new Set(); @@ -63,8 +67,8 @@ export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) { const todo = [ ...(user.spec.memberOf ?? []), ...groups - .filter(g => g.spec.members?.includes(user.metadata.name)) - .map(g => g.metadata.name), + .filter(g => g.spec.members?.includes(stringifyEntityRef(user))) + .map(g => stringifyEntityRef(g)), ]; for (;;) { @@ -75,7 +79,7 @@ export function buildMemberOf(groups: GroupEntity[], users: UserEntity[]) { if (!transitiveMemberOf.has(current)) { transitiveMemberOf.add(current); - const group = groupsByName.get(current); + const group = groupsByRef.get(current); if (group?.spec.parent) { todo.push(group.spec.parent); } diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts index 3ded4635a0..410ac847b3 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -362,8 +362,8 @@ describe('resolveRelations', () => { ['pa', new Set(['ca'])], ]); resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }); it('matches by UUID', () => { @@ -383,8 +383,8 @@ describe('resolveRelations', () => { ['pa', new Set(['ca'])], ]); resolveRelations([parent, child], [], new Map(), new Map(), groupMember); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }); }); @@ -403,7 +403,7 @@ describe('resolveRelations', () => { ['ma', new Set(['ha'])], ]); resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['host']); + expect(member.spec.memberOf).toEqual(['group:default/host']); }); it('populates relations by uuid', () => { @@ -423,7 +423,7 @@ describe('resolveRelations', () => { ['ma', new Set(['ha'])], ]); resolveRelations([host], [member], userMemberOf, new Map(), new Map()); - expect(member.spec.memberOf).toEqual(['host']); + expect(member.spec.memberOf).toEqual(['group:default/host']); }); }); @@ -451,8 +451,8 @@ describe('resolveRelations', () => { groupMemberOf, new Map(), ); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }); }); @@ -473,8 +473,8 @@ describe('resolveRelations', () => { ['ca', new Set(['pa'])], ]); resolveRelations([parent, child], [], new Map(), groupMemberOf, new Map()); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); }); describe('groupMember', () => { @@ -507,9 +507,9 @@ describe('resolveRelations', () => { new Map(), groupMember, ); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); - expect(member.spec.memberOf).toEqual(['parent']); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + expect(member.spec.memberOf).toEqual(['group:default/parent']); }); it('populates relations by uuid', () => { @@ -541,9 +541,9 @@ describe('resolveRelations', () => { new Map(), groupMember, ); - expect(parent.spec.children).toEqual(['child']); - expect(child.spec.parent).toEqual('parent'); - expect(member.spec.memberOf).toEqual(['parent']); + expect(parent.spec.children).toEqual(['group:default/child']); + expect(child.spec.parent).toEqual('group:default/parent'); + expect(member.spec.memberOf).toEqual(['group:default/parent']); }); }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index e15ec85554..d0fe30e0e6 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + GroupEntity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; import { SearchEntry } from 'ldapjs'; import lodashSet from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; @@ -353,18 +357,18 @@ export function resolveRelations( ) { // Build reference lookup tables - all of the relations that are output from // the above calls can be expressed as either DNs or UUIDs so we need to be - // able to find by both, as well as the name. Note that we expect them to not + // able to find by both, as well as the entity reference. Note that we expect them to not // collide here - this is a reasonable assumption as long as the fields are // the supported forms. - const userMap: Map = new Map(); // by name, dn, uuid - const groupMap: Map = new Map(); // by name, dn, uuid + const userMap: Map = new Map(); // by entityRef, dn, uuid + const groupMap: Map = new Map(); // by entityRef, dn, uuid for (const user of users) { - userMap.set(user.metadata.name, user); + userMap.set(stringifyEntityRef(user), user); userMap.set(user.metadata.annotations![LDAP_DN_ANNOTATION], user); userMap.set(user.metadata.annotations![LDAP_UUID_ANNOTATION], user); } for (const group of groups) { - groupMap.set(group.metadata.name, group); + groupMap.set(stringifyEntityRef(group), group); groupMap.set(group.metadata.annotations![LDAP_DN_ANNOTATION], group); groupMap.set(group.metadata.annotations![LDAP_UUID_ANNOTATION], group); } @@ -375,7 +379,7 @@ export function resolveRelations( userMap.delete(undefined!); groupMap.delete(undefined!); - // Fill in all of the immediate relations, now keyed on metadata.name. We + // Fill in all of the immediate relations, now keyed on the entity reference. We // keep all parents at this point, whether the target model can support more // than one or not (it gets filtered farther down). And group children are // only groups in here. @@ -394,8 +398,8 @@ export function resolveRelations( for (const groupN of groupsN) { const group = groupMap.get(groupN); if (group) { - ensureItems(newUserMemberOf, user.metadata.name, [ - group.metadata.name, + ensureItems(newUserMemberOf, stringifyEntityRef(user), [ + stringifyEntityRef(group), ]); } } @@ -407,11 +411,11 @@ export function resolveRelations( for (const parentN of parentsN) { const parentGroup = groupMap.get(parentN); if (parentGroup) { - ensureItems(newGroupParents, group.metadata.name, [ - parentGroup.metadata.name, + ensureItems(newGroupParents, stringifyEntityRef(group), [ + stringifyEntityRef(parentGroup), ]); - ensureItems(newGroupChildren, parentGroup.metadata.name, [ - group.metadata.name, + ensureItems(newGroupChildren, stringifyEntityRef(parentGroup), [ + stringifyEntityRef(group), ]); } } @@ -425,17 +429,17 @@ export function resolveRelations( // try both const memberUser = userMap.get(memberN); if (memberUser) { - ensureItems(newUserMemberOf, memberUser.metadata.name, [ - group.metadata.name, + ensureItems(newUserMemberOf, stringifyEntityRef(memberUser), [ + stringifyEntityRef(group), ]); } else { const memberGroup = groupMap.get(memberN); if (memberGroup) { - ensureItems(newGroupChildren, group.metadata.name, [ - memberGroup.metadata.name, + ensureItems(newGroupChildren, stringifyEntityRef(group), [ + stringifyEntityRef(memberGroup), ]); - ensureItems(newGroupParents, memberGroup.metadata.name, [ - group.metadata.name, + ensureItems(newGroupParents, stringifyEntityRef(memberGroup), [ + stringifyEntityRef(group), ]); } } diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 4a0c147854..b0fe7bd39e 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.3.2-next.0 + +### Patch Changes + +- 8d9f673106: Add annotation `microsoft.com/email` when using the `defaultUserTransformer`. + + This will allow users of the Microsoft auth provider to utilize the predefined + SignIn resolver instead of maintaining their own. + + ```typescript + // backend/plugins/auth.ts + + // [...] + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + // [...] + providerFactories: { + microsoft: providers.microsoft.create({ + signIn: { + resolver: + providers.microsoft.resolvers.emailMatchingUserEntityAnnotation(), + }, + }), + }, + }); + } + ``` + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.2-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + +## 0.3.1 + +### Patch Changes + +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` +- 85fc53df95: Now plugin configuration accept a new optional parameter `groupSelect` which allow the client to fetch defined fields from the ms-graph api. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + +## 0.3.1-next.2 + +### Patch Changes + +- 85fc53df95: Now plugin configuration accept a new optional parameter `groupSelect` which allow the client to fetch defined fields from the ms-graph api. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0-next.3 + ## 0.3.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 2239ec1332..31a22b0bc4 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -49,6 +49,9 @@ export type GroupTransformer = ( groupPhoto?: string, ) => Promise; +// @public +export const MICROSOFT_EMAIL_ANNOTATION = 'microsoft.com/email'; + // @public export const MICROSOFT_GRAPH_GROUP_ID_ANNOTATION = 'graph.microsoft.com/group-id'; diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 0afaf27f75..903260707b 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.3.1-next.1", + "version": "0.3.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/backend-tasks": "^0.3.0-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/plugin-catalog-backend": "^1.1.0-next.1", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", @@ -48,9 +48,9 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts index bbfbd61efe..dc8ac6ef62 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/constants.ts @@ -14,6 +14,13 @@ * limitations under the License. */ +/** + * The (primary) user email. Also used by the Microsoft auth provider to resolve the User entity. + * + * @public + */ +export const MICROSOFT_EMAIL_ANNOTATION = 'microsoft.com/email'; + /** * The tenant id used by the Microsoft Graph API * diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index 61e62f5803..e9ab49f7d1 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -19,6 +19,7 @@ export type { GroupMember, ODataQuery } from './client'; export { readMicrosoftGraphConfig } from './config'; export type { MicrosoftGraphProviderConfig } from './config'; export { + MICROSOFT_EMAIL_ANNOTATION, MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, MICROSOFT_GRAPH_USER_ID_ANNOTATION, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 1fc4904b74..caef1701f6 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -96,6 +96,7 @@ describe('read microsoft graph', () => { metadata: { annotations: { 'graph.microsoft.com/user-id': 'userid', + 'microsoft.com/email': 'user.name@example.com', }, name: 'user.name_example.com', }, @@ -146,6 +147,7 @@ describe('read microsoft graph', () => { metadata: { annotations: { 'graph.microsoft.com/user-id': 'userid', + 'microsoft.com/email': 'user.name@example.com', }, name: 'user.name_example.com', }, @@ -263,6 +265,7 @@ describe('read microsoft graph', () => { metadata: { annotations: { 'graph.microsoft.com/user-id': 'userid', + 'microsoft.com/email': 'user.name@example.com', }, name: 'user.name_example.com', }, @@ -339,6 +342,7 @@ describe('read microsoft graph', () => { metadata: { annotations: { 'graph.microsoft.com/user-id': 'userid', + 'microsoft.com/email': 'user.name@example.com', }, name: 'user.name_example.com', }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index de7f3e49e0..ecf74b34d9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -24,6 +24,7 @@ import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import { MicrosoftGraphClient } from './client'; import { + MICROSOFT_EMAIL_ANNOTATION, MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, MICROSOFT_GRAPH_TENANT_ID_ANNOTATION, MICROSOFT_GRAPH_USER_ID_ANNOTATION, @@ -57,6 +58,7 @@ export async function defaultUserTransformer( metadata: { name, annotations: { + [MICROSOFT_EMAIL_ANNOTATION]: user.mail!, [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: user.id!, }, }, diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index d97bda2dad..ebec4cac05 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,92 @@ # @backstage/plugin-catalog-backend +## 1.1.2-next.0 + +### Patch Changes + +- 55e09b29dd: Fixing broken types for `knex` when checking returned rows +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 8012ac46a0: **BREAKING (alpha api):** Replace `createCatalogPolicyDecision` export with `createCatalogConditionalDecision`, which accepts a permission parameter of type `ResourcePermission<'catalog-entity'>` along with conditions. The permission passed is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: + + ```typescript + class TestPermissionPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + _user?: BackstageIdentityResponse, + ): Promise { + if ( + // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> + isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) + ) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner( + _user?.identity.ownershipEntityRefs ?? [], + ), + ); + } + + return { + result: AuthorizeResult.ALLOW, + }; + ``` + +- 8012ac46a0: **BREAKING:** Mark CatalogBuilder#addPermissionRules as @alpha. +- fb02d2d94d: export `locationSpecToLocationEntity` +- bf82edf4c9: Added `/validate-entity` endpoint + +### Patch Changes + +- ada4446733: Specify type of `visibilityPermission` property on collators and collator factories. +- 1691c6c5c2: Clarify that config locations that emit User and Group kinds now need to declare so in the `catalog.locations.[].rules` +- 8592cacfd3: Fixed an issue where sometimes entities would have stale relations "stuck" and + not getting removed as expected, after the other end of the relation had stopped + referring to them. +- 23646e51a5: Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions. +- 9fe24b0fc8: Adjust the error messages when entities fail validation, to clearly state what entity that failed it +- 48405ed232: Added `spec.profile.displayName` to search index for Group kinds +- 95408dbe99: Enable internal batching of very large deletions, to not run into SQL binding limits +- 8012ac46a0: Handle changes to @alpha permission-related types. + + - All exported permission rules and conditions now have a `resourceType`. + - `createCatalogConditionalDecision` now expects supplied conditions to have the appropriate `resourceType`. + - `createCatalogPermissionRule` now expects `resourceType` as part of the supplied rule object. + - Introduce new `CatalogPermissionRule` convenience type. + +- ffec894ed0: add gitlab to AnnotateScmSlugEntityProcessor +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + +## 1.1.0-next.3 + +### Patch Changes + +- 23646e51a5: Use new `PermissionEvaluator#authorizeConditional` method when retrieving permission conditions. +- 48405ed232: Added `spec.profile.displayName` to search index for Group kinds +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/plugin-permission-node@0.6.0-next.2 + - @backstage/backend-common@0.13.2-next.2 + - @backstage/integration@1.1.0-next.2 + ## 1.1.0-next.2 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index fa514ccc93..c0c74677c6 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.1.0-next.2", + "version": "1.1.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,17 +34,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-node": "^0.6.0-next.1", - "@backstage/plugin-scaffolder-common": "^1.0.1-next.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-node": "^0.6.1-next.0", + "@backstage/plugin-scaffolder-common": "^1.0.1", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/types": "^1.0.0", "@types/express": "^4.17.6", "codeowners-utils": "^1.0.2", @@ -52,7 +52,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "git-url-parse": "^11.6.0", "glob": "^7.1.6", "knex": "^1.0.2", @@ -68,10 +68,10 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.2", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-search-backend-node": "0.5.3-next.0", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-search-backend-node": "0.6.1-next.0", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f5f00cd0fc..57508615ac 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -382,7 +382,7 @@ export class CatalogBuilder { const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog(dbClient); let permissionEvaluator: PermissionEvaluator; - if ('query' in permissions) { + if ('authorizeConditional' in permissions) { permissionEvaluator = permissions as PermissionEvaluator; } else { logger.warn( diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index c31052c01e..cf65a14fee 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -213,7 +213,7 @@ export class Stitcher { // to write the search index. const searchEntries = buildEntitySearch(entityId, entity); - const rowsChanged = await this.database( + const amountOfRowsChanged = await this.database( 'final_entities', ) .update({ @@ -225,7 +225,7 @@ export class Stitcher { .onConflict('entity_id') .merge(['final_entity', 'hash']); - if (rowsChanged.length === 0) { + if (amountOfRowsChanged === 0) { this.logger.debug( `Entity ${entityRef} is already processed, skipping write.`, ); diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index d362c20d27..6054a0f92b 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-common +## 1.0.1 + +### Patch Changes + +- ada4446733: Use `createPermission` helper when creating permissions. +- 8c8bee47f4: Add `@alpha` `CatalogEntityPermission` convenience type, available for import from `@backstage/plugin-catalog-common/alpha`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/search-common@0.3.3 + ## 1.0.1-next.1 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 2db301a8fd..2ef638a106 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "1.0.1-next.1", + "version": "1.0.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/search-common": "^0.3.3-next.1" + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/search-common": "^0.3.3" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index a998c0ac1f..c44db1691b 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-catalog-graph +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.2.16 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 77800a32dd: Added renderNode and renderLabel property to EntityRelationsGraph to support customization using CustomNode and CustomLabel components +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/catalog-client@1.0.1 + +## 0.2.16-next.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.2.16-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 68b92f57af..9a9a31c5da 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.16-next.2", + "version": "0.2.17-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,11 +24,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,14 +45,14 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/plugin-catalog": "^1.1.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7" }, diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 21bbca62ef..89601ac921 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-graphql +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 0f5fad7a14..eadcc9bf44 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.8-next.0", + "version": "0.3.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,26 +34,25 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/types": "^1.0.0", - "graphql-modules": "^2.0.0", "apollo-server": "^3.0.0", "graphql": "^16.0.0", + "graphql-modules": "^2.0.0", "graphql-tag": "^2.11.0", "graphql-type-json": "^0.3.2", "node-fetch": "^2.6.7", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@graphql-codegen/cli": "^2.3.1", + "@graphql-codegen/graphql-modules-preset": "^2.3.2", "@graphql-codegen/typescript": "^2.4.2", "@graphql-codegen/typescript-resolvers": "^2.4.3", - "@graphql-codegen/graphql-modules-preset": "^2.3.2", "@graphql-tools/schema": "^8.3.1", - "eslint-plugin-graphql": "^4.0.0", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 68dcb89982..b6a6e3ec73 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-catalog-import +## 0.8.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## 0.8.7 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/catalog-client@1.0.1 + +## 0.8.7-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/integration@1.1.0-next.2 + ## 0.8.7-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 7ecad7a0cd..3d66e2e5b5 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.8.7-next.1", + "version": "0.8.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -60,13 +60,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "cross-fetch": "^3.1.5", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 3c6c731868..1d63bdc1c2 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-catalog-react +## 1.1.0-next.0 + +### Minor Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## 1.0.1 + +### Patch Changes + +- 0ffd88a90e: Prevent permissions with types other than `ResourcePermission<'catalog-entity'>` from being used with the `useEntityPermission` hook. +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 4af82967f4: Decouple tags picker from backend entities + + `EntityTagPicker` fetches all the tags independently and it doesn't require all the entities to be available client side. + +- 4be0d1e777: Changed catalog filter components to only pay attention to query parameters relevant to the component. +- 5d5fdbe541: Columns in CatalogTable now change depending on the entity kind, ensuring only relevant columns are displayed. +- 863e7bcb7b: Updated the "unregister location" behavior in `UnregisterEntityDialog`. Removed unnecessary entity deletion requests that were sent after successfully deleting a location. +- 37b04b5a5e: Removed broken link from Labels section of entity inspector. +- a496cee4d1: Add support for string refs to the `EntityRefLinks` component +- d34900af81: Added a new `NextScaffolderRouter` which will eventually replace the exiting router +- 99063c39ae: Minor API report cleanup +- 4431873583: Update `usePermission` usage. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + +## 1.0.1-next.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 863e7bcb7b: Updated the "unregister location" behavior in `UnregisterEntityDialog`. Removed unnecessary entity deletion requests that were sent after successfully deleting a location. +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/plugin-permission-react@0.4.0-next.1 + - @backstage/integration@1.1.0-next.2 + ## 1.0.1-next.2 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 831a34edc8..285ecafd70 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -13,6 +13,7 @@ import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import { IconButton } from '@material-ui/core'; +import { InfoCardVariants } from '@backstage/core-components'; import { LinkProps } from '@backstage/core-components'; import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; @@ -333,7 +334,7 @@ export interface EntityTableProps { // (undocumented) title: string; // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3b34ee16e1..7b0f2105fa 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.0.1-next.2", + "version": "1.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,18 +34,18 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-react": "^0.4.0-next.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-react": "^0.4.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", - "@backstage/version-bridge": "^1.0.0", + "@backstage/version-bridge": "^1.0.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -63,14 +63,14 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-scaffolder-common": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-scaffolder-common": "^1.0.1", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/jwt-decode": "^3.1.0", diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx index 879a35ffd0..3aa6857120 100644 --- a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx @@ -19,7 +19,11 @@ import { makeStyles } from '@material-ui/core'; import React, { ReactNode } from 'react'; import { columnFactories } from './columns'; import { componentEntityColumns, systemEntityColumns } from './presets'; -import { Table, TableColumn } from '@backstage/core-components'; +import { + InfoCardVariants, + Table, + TableColumn, +} from '@backstage/core-components'; /** * Props for {@link EntityTable}. @@ -28,7 +32,7 @@ import { Table, TableColumn } from '@backstage/core-components'; */ export interface EntityTableProps { title: string; - variant?: 'gridItem'; + variant?: InfoCardVariants; entities: T[]; emptyContent?: ReactNode; columns: TableColumn[]; diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 7b1821d026..2a54a03025 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-catalog +## 1.2.0-next.0 + +### Minor Changes + +- 4274844a8c: Use InfoCardVariants on custom cards variant attribute +- ceca63d149: Added the prop `NotFoundComponent` to `EntityLayout` which can be used to include a custom component when an entity is not found in the catalog + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## 1.1.0 + +### Minor Changes + +- bdc61b4002: Expose 'initalFilter' through initialKind prop on Catalog Page. + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 2a3cac4c60: Corrected replacements for depreciated FilteredEntityLayout items +- f6d2694092: Added tableOptions property to CatalogTable and DefaultCatalogPage to support customization of the Catalog Table. Related issue #10453 +- 5d5fdbe541: Columns in CatalogTable now change depending on the entity kind, ensuring only relevant columns are displayed. +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + +## 1.1.0-next.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 1.1.0-next.2 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index d1e733a553..e8da88a4e1 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -184,7 +184,7 @@ export interface DependencyOfComponentsCardProps { // (undocumented) title?: string; // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) @@ -192,13 +192,13 @@ export interface DependsOnComponentsCardProps { // (undocumented) title?: string; // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) export interface DependsOnResourcesCardProps { // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) @@ -247,6 +247,8 @@ export const EntityLayout: { export interface EntityLayoutProps { // (undocumented) children?: React_2.ReactNode; + // (undocumented) + NotFoundComponent?: React_2.ReactNode; // Warning: (ae-forgotten-export) The symbol "contextMenuOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -283,7 +285,7 @@ export interface EntityLinksCardProps { // (undocumented) cols?: ColumnBreakpoints | number; // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) @@ -344,25 +346,25 @@ export function hasCatalogProcessingErrors( // @public (undocumented) export interface HasComponentsCardProps { // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) export interface HasResourcesCardProps { // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) export interface HasSubcomponentsCardProps { // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public (undocumented) export interface HasSystemsCardProps { // (undocumented) - variant?: 'gridItem'; + variant?: InfoCardVariants; } // @public @@ -390,7 +392,7 @@ export const RelatedEntitiesCard: ( // @public (undocumented) export type RelatedEntitiesCardProps = { - variant?: 'gridItem'; + variant?: InfoCardVariants; title: string; columns: TableColumn[]; entityKind?: string; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 1fe0f081b4..87ee486960 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.1.0-next.2", + "version": "1.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", @@ -60,11 +60,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/plugin-permission-react": "^0.4.0-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/plugin-permission-react": "^0.4.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx index b85a446b75..d87b558048 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_DEPENDENCY_OF } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface DependencyOfComponentsCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; title?: string; } diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index 33e8caf870..4f30c17a84 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_DEPENDS_ON } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface DependsOnComponentsCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; title?: string; } diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index 3fe29093fe..a0f4d399f9 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_DEPENDS_ON } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asResourceEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface DependsOnResourcesCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index c1deaf4b74..0a3f443d3e 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -106,7 +106,7 @@ describe('EntityLayout', () => { expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); }); - it('renders error message when entity is not found', async () => { + it('renders default error message when entity is not found', async () => { const rendered = await renderInTestApp( @@ -130,6 +130,34 @@ describe('EntityLayout', () => { expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); }); + it('renders custom message when entity is not found', async () => { + const rendered = await renderInTestApp( + + + Oppps.. Your entity was not found
} + > + +
tabbed-test-content
+
+ + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect( + rendered.getByText('Oppps.. Your entity was not found'), + ).toBeInTheDocument(); + expect(rendered.queryByText('my-entity')).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + }); + it('navigates when user clicks different tab', async () => { const rendered = await renderInTestApp( diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index d4244419cb..693137fe21 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -152,6 +152,7 @@ export interface EntityLayoutProps { UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; UNSTABLE_contextMenuOptions?: contextMenuOptions; children?: React.ReactNode; + NotFoundComponent?: React.ReactNode; } /** @@ -176,6 +177,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, children, + NotFoundComponent, } = props; const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); @@ -265,13 +267,17 @@ export const EntityLayout = (props: EntityLayoutProps) => { {!loading && !error && !entity && ( - - There is no {kind} with the requested{' '} - - kind, namespace, and name - - . - + {NotFoundComponent ? ( + NotFoundComponent + ) : ( + + There is no {kind} with the requested{' '} + + kind, namespace, and name + + . + + )} )} diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index 4c4a1ac908..10302d7837 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -21,12 +21,12 @@ import { EntityLinksEmptyState } from './EntityLinksEmptyState'; import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; -import { InfoCard } from '@backstage/core-components'; +import { InfoCard, InfoCardVariants } from '@backstage/core-components'; /** @public */ export interface EntityLinksCardProps { cols?: ColumnBreakpoints | number; - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function EntityLinksCard(props: EntityLinksCardProps) { diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 5864028694..9654152a5c 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface HasComponentsCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function HasComponentsCard(props: HasComponentsCardProps) { diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index 19a48aad25..8476d77bda 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asResourceEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface HasResourcesCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function HasResourcesCard(props: HasResourcesCardProps) { diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index 0b31bbd2be..db95d31f9f 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -24,7 +25,7 @@ import { /** @public */ export interface HasSubcomponentsCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) { diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index 7ab6831e42..5d6fa170f5 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -15,6 +15,7 @@ */ import { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants } from '@backstage/core-components'; import React from 'react'; import { asSystemEntities, @@ -25,7 +26,7 @@ import { /** @public */ export interface HasSystemsCardProps { - variant?: 'gridItem'; + variant?: InfoCardVariants; } export function HasSystemsCard(props: HasSystemsCardProps) { diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index 70854c1b21..e802b0b583 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -24,6 +24,7 @@ import { import React from 'react'; import { InfoCard, + InfoCardVariants, Link, Progress, ResponseErrorPanel, @@ -32,7 +33,7 @@ import { /** @public */ export type RelatedEntitiesCardProps = { - variant?: 'gridItem'; + variant?: InfoCardVariants; title: string; columns: TableColumn[]; entityKind?: string; diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md new file mode 100644 index 0000000000..2cc84be0eb --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -0,0 +1,35 @@ +# @backstage/plugin-cicd-statistics-module-gitlab + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.7-next.0 + +## 0.1.0 + +### Minor Changes + +- d3af30b47d: Created a module to extract the CI/CD statistics from a Gitlab repository. + Read the `README.md` in the `cicd-statistics-module-gitlab` plugin folder on how to set it up. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-cicd-statistics@0.1.6 + +## 0.1.0-next.0 + +### Minor Changes + +- d3af30b47d: Created a module to extract the CI/CD statistics from a Gitlab repository. + Read the `README.md` in the `cicd-statistics-module-gitlab` plugin folder on how to set it up. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-cicd-statistics@0.1.6-next.1 diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index b36bcf2476..b083359a55 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.0.0", + "version": "0.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,16 +29,16 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@backstage/plugin-cicd-statistics": "^0.1.6-next.0", + "@backstage/plugin-cicd-statistics": "^0.1.7-next.0", "@gitbeaker/browser": "^35.6.0", "@gitbeaker/core": "^35.6.0", "luxon": "^2.0.2", "p-limit": "^4.0.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/catalog-model": "^1.0.1-next.1" + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/catalog-model": "^1.0.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 9b6f244eb8..2447704019 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-cicd-statistics +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.6-next.0 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index bb80e7674a..ceceb1e60c 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.6-next.0", + "version": "0.1.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,9 +37,9 @@ "@types/luxon": "^2.0.5" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@date-io/luxon": "^1.3.13", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 3fd6adf138..ae34fa0fac 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-circleci +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 56bbd007f0..380759ed4d 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index e197b795ef..a002dae9d2 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-cloudbuild +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 49b58b0ae1..25d63b28c1 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index df7dc94d86..a53376e1e9 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-code-climate +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index b09be97d3b..61cc5f2b3d 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.4-next.0", + "version": "0.1.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,10 +23,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,9 +40,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 52420f5f09..0f9ba24be9 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-code-coverage-backend +## 0.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + +## 0.1.29 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 5a31ecead8..59e4592d7b 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.29-next.1", + "version": "0.1.30-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,12 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 9ba5be81c7..a6d96bfbd8 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-code-coverage +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.31-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.31-next.0 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 89065b231a..a9c86e2cb2 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.31-next.0", + "version": "0.1.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,12 +24,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index cfb9b0553d..2bee04cb31 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-config-schema +## 0.1.27 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.27-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.1.27-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 9c8945af29..64e1a140fa 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.27-next.0", + "version": "0.1.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", @@ -41,10 +41,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index f632dd29f2..1c654e62b7 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-cost-insights +## 0.11.26 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.11.26-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.11.26-next.0 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index fca3059317..474b58e1ab 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.11.26-next.0", + "version": "0.11.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -60,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/cost-insights/src/alerts/README.md b/plugins/cost-insights/src/alerts/README.md index 4b24e4005f..31efe57797 100644 --- a/plugins/cost-insights/src/alerts/README.md +++ b/plugins/cost-insights/src/alerts/README.md @@ -35,7 +35,7 @@ export class CostInsightsClient extends CostInsightsApi { ### Custom Setup -Default properties such as the title, subtitle and instructions page url can be overridden - even default UI such as chart itself. Additionally, alerts can be extended to support actions such as snoozing or dismissing. +Default properties such as the title, subtitle and instructions page URL can be overridden - even default UI such as chart itself. Additionally, alerts can be extended to support actions such as snoozing or dismissing. ![project-growth-alert-custom](../assets/project-growth-alert-custom.png) diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 696882ffb1..9e033f4fcb 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-explore-react +## 0.0.16 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-plugin-api@1.0.1 + +## 0.0.16-next.0 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.0.15 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 1d9611803d..3bd5947163 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.15", + "version": "0.0.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-plugin-api": "^1.0.0" + "@backstage/core-plugin-api": "^1.0.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index b990926fe7..c173f9eab2 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-explore +## 0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.35 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-explore-react@0.0.16 + +## 0.3.35-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-explore-react@0.0.16-next.0 + ## 0.3.35-next.0 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index f4845f433c..f590d72190 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.35-next.0", + "version": "0.3.36-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/plugin-explore-react": "^0.0.15", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-explore-react": "^0.0.16", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 61c74682f2..95587bff6b 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-firehydrant +## 0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.21 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.21-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.21-next.0 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 86c22d9c40..b0b022ffbd 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.21-next.0", + "version": "0.1.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,9 +25,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,10 +39,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 4bfb9a152b..cbc337ab64 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-fossa +## 0.2.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.2.36 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.2.36-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.2.36-next.0 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 49e14dd3f0..af76c0b952 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.36-next.0", + "version": "0.2.37-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index 6b8fcc5e3a..351d12c451 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-gcalendar +## 0.3.1-next.0 + +### Patch Changes + +- 1429dbbea9: fixed google API types dependencies + +## 0.3.0 + +### Minor Changes + +- c6616e6fc9: Fixed issue when not all calendars were fetched for some accounts + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.0-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.3.0-next.1 ### Minor Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index 103d63a01d..b7aa5c7674 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.0-next.1", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,13 +22,14 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.0", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "@maxim_mazurok/gapi.client.calendar": "^3.0.20220408", "classnames": "^2.3.1", "cross-fetch": "^3.1.5", "dompurify": "^2.3.6", @@ -42,17 +43,14 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/dompurify": "^2.3.3", - "@types/gapi": "^0.0.41", - "@types/gapi.auth2": "^0.0.56", - "@types/gapi.client.calendar": "^3.0.10", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", "@types/sanitize-html": "^2.6.2", diff --git a/plugins/gcalendar/src/api/types.ts b/plugins/gcalendar/src/api/types.ts index b6d9a36eb9..5c8bbdd1c1 100644 --- a/plugins/gcalendar/src/api/types.ts +++ b/plugins/gcalendar/src/api/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -/// -/// +/// export type GCalendarList = gapi.client.calendar.CalendarList; diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index e510ca45e0..3b91446e54 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-gcp-projects +## 0.3.23 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.23-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.3.23-next.0 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index f5f81660b8..2f6800083a 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.23-next.0", + "version": "0.3.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 9bcc2a5c79..eb8b19a96a 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-git-release-manager +## 0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + +## 0.3.17 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.17-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration@1.1.0-next.2 + ## 0.3.17-next.1 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 701ceb8999..a1948a66fc 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.17-next.1", + "version": "0.3.18-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,9 +24,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,13 +43,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 8c3c98531d..3a64b5e519 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-github-actions +## 0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.5.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.5.4-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/integration@1.1.0-next.2 + ## 0.5.4-next.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index fca4ab1f4e..da0bacebd5 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.5.4-next.1", + "version": "0.5.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,11 +36,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 197e25b88e..8456d274d6 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-github-deployments +## 0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## 0.1.35 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + +## 0.1.35-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/integration@1.1.0-next.2 + ## 0.1.35-next.1 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 3f7098f85d..7ce8422b0a 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.35-next.1", + "version": "0.1.36-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 458650c75f..051a15e183 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-gitops-profiles +## 0.3.22 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.22-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.3.22-next.0 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index cda82cefb7..f5d8020d84 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.22-next.0", + "version": "0.3.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 09f8a66e06..45c6f2fb60 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-gocd +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.10 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 2677db7c15: Add pagination controls to GoCD build table +- 4c6c2b2918: Add DORA metrics insights to GoCD builds page +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.10-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 4c6c2b2918: Add DORA metrics insights to GoCD builds page +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index 1920ca4995..fff85992f3 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.10-next.1", + "version": "0.1.11-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 2017319db3..fd3b55766a 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-graphiql +## 0.2.36 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.2.36-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.2.36-next.1 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 9d6a07b4e5..2c6a1f883d 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.36-next.1", + "version": "0.2.36", "private": false, "publishConfig": { "access": "public", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index af1434a1a9..a55bf5897d 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-graphql-backend +## 0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-graphql@0.3.8 + ## 0.1.21-next.0 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index a4dd95076b..3abf17b87a 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-backend", "description": "An experimental Backstage backend plugin for GraphQL", - "version": "0.1.21-next.0", + "version": "0.1.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,26 +34,25 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", - "@backstage/plugin-catalog-graphql": "^0.3.8-next.0", + "@backstage/plugin-catalog-graphql": "^0.3.8", "@graphql-tools/schema": "^8.3.1", - "graphql-modules": "^2.0.0", "@types/express": "^4.17.6", "apollo-server": "^3.0.0", "apollo-server-express": "^3.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", "graphql": "^16.0.0", + "graphql-modules": "^2.0.0", "helmet": "^5.0.2", "reflect-metadata": "^0.1.13", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", - "eslint-plugin-graphql": "^4.0.0", "msw": "^0.35.0", "supertest": "^6.1.3" }, diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 969bdcefc7..125c427aee 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-home +## 0.4.21-next.0 + +### Patch Changes + +- 2b733d9d39: star icons now have the same yellow color as the other star icons when a entity is favourite +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/plugin-stack-overflow@0.1.1-next.0 + +## 0.4.20 + +### Patch Changes + +- ab230a433f: Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package. +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- ac323de4ad: - Adds new `HomePageStackOverflowQuestions` component which renders a list of stack overflow questions on your homepage. + + - Exports `ComponentRenderer` type. + +- 230ad0826f: Bump to using `@types/node` v16 +- 9b58775849: Updated the dependency on `@backstage/config` to `^1.0.0`. +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-stack-overflow@0.1.0 + +## 0.4.20-next.3 + +### Patch Changes + +- ab230a433f: Export template logos `TemplateBackstageLogo` and `TemplateBackstageLogoIcon` from package. +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 9b58775849: Updated the dependency on `@backstage/config` to `^1.0.0`. +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-stack-overflow@0.1.0-next.1 + ## 0.4.20-next.2 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index aac39c397a..ce21cb36cb 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.20-next.2", + "version": "0.4.21-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", - "@backstage/plugin-stack-overflow": "^0.1.0-next.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-stack-overflow": "^0.1.1-next.0", "@backstage/theme": "^0.2.15", "@backstage/config": "^1.0.0", "@material-ui/core": "^4.12.2", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx index 6c9775c842..e82d915e3b 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx @@ -38,7 +38,6 @@ import React from 'react'; * * @public */ - export const Content = () => { const catalogEntityRoute = useRouteRef(entityRouteRef); const { starredEntities, toggleStarredEntity } = useStarredEntities(); @@ -64,7 +63,7 @@ export const Content = () => { aria-label="unstar" onClick={() => toggleStarredEntity(entity)} > - + diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 520feb9863..4b99a56a87 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-ilert +## 0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.30 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.30-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.30-next.0 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index e66020d3ac..f410123a86 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.30-next.0", + "version": "0.1.31-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,11 +24,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 34d3a54730..10afcd6c33 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-jenkins-backend +## 0.1.22-next.0 + +### Patch Changes + +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## 0.1.20 + +### Patch Changes + +- 1917923ab8: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- b013de3f50: feature: provide access token to JenkinsInstanceConfig. It can be passed to other backend calls if authentication enabled. DefaultJenkinsInfoProvider sends always this token to catalog api if access token exists. +- ca91107110: Fixed possible type error if jenkins response contains null values +- 71f8708f00: Make `resourceRef` required in `JenkinsApi` to match usage. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-jenkins-common@0.1.3 + - @backstage/catalog-client@1.0.1 + +## 0.1.20-next.2 + +### Patch Changes + +- 1917923ab8: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- b013de3f50: feature: provide access token to JenkinsInstanceConfig. It can be passed to other backend calls if authentication enabled. DefaultJenkinsInfoProvider sends always this token to catalog api if access token exists. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/backend-common@0.13.2-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index f326ce2247..861c7fddd6 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.20-next.1", + "version": "0.1.22-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,14 +25,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/plugin-jenkins-common": "^0.1.3-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/plugin-jenkins-common": "^0.1.3", + "@backstage/plugin-permission-common": "^0.6.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -41,7 +41,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index f6bd21c633..fa357f8e9f 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -40,7 +40,7 @@ export async function createRouter( const { jenkinsInfoProvider, permissions, logger } = options; let permissionEvaluator: PermissionEvaluator | undefined; - if (permissions && 'query' in permissions) { + if (permissions && 'authorizeConditional' in permissions) { permissionEvaluator = permissions as PermissionEvaluator; } else { logger.warn( diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index 6f7cff53bb..a5883d28f8 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins-common +## 0.1.3 + +### Patch Changes + +- ada4446733: Use `createPermission` helper when creating permissions. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-catalog-common@1.0.1 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 342d645125..a8c04efef0 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.3-next.1", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0" + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 3ce8278e7c..62b27ecb2d 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-jenkins +## 0.7.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.7.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-jenkins-common@0.1.3 + +## 0.7.3-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.7.3-next.0 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index e7170c012a..1cbe6dd327 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.7.3-next.0", + "version": "0.7.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,12 +35,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/plugin-jenkins-common": "^0.1.3-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-jenkins-common": "^0.1.3", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,10 +54,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 6a413e6925..61124c0c96 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-kafka-backend +## 0.2.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.2.24 + +### Patch Changes + +- f26cf63878: build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0 +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + +## 0.2.24-next.1 + +### Patch Changes + +- f26cf63878: build(deps-dev): bump `@types/jest-when` from 2.7.2 to 3.5.0 +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 732fc30927..b3f3e0c880 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.24-next.0", + "version": "0.2.25-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest-when": "^3.5.0", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 14c7883ce2..67f8e97b7c 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-kafka +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 5603ad7588..edd2549567 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,13 +39,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 177bcca549..d344d8c63d 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,151 @@ # @backstage/plugin-kubernetes-backend +## 0.5.1-next.0 + +### Patch Changes + +- b9f7ffb162: Add filtering to GKE cluster locator +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.5.0 + +### Minor Changes + +- 3d45427666: **BREAKING** Custom cluster suppliers need to cache their getClusters result + + To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters + the `getClusters` method is now called whenever the list of clusters is needed. + + Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters` + can be called frequently and should return a cached result from `getClusters` instead. + + For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`: + + ```diff + -import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; + +import { + + ClusterDetails, + + KubernetesBuilder, + + KubernetesClustersSupplier, + +} from '@backstage/plugin-kubernetes-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + +import { Duration } from 'luxon'; + + + +export class CustomClustersSupplier implements KubernetesClustersSupplier { + + constructor(private clusterDetails: ClusterDetails[] = []) {} + + + + static create(refreshInterval: Duration) { + + const clusterSupplier = new CustomClustersSupplier(); + + // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts + + runPeriodically( + + () => clusterSupplier.refreshClusters(), + + refreshInterval.toMillis(), + + ); + + return clusterSupplier; + + } + + + + async refreshClusters(): Promise { + + this.clusterDetails = []; // fetch from somewhere + + } + + + + async getClusters(): Promise { + + return this.clusterDetails; + + } + +} + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + - const { router } = await KubernetesBuilder.createBuilder({ + + const builder = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + - }).build(); + + }); + + builder.setClusterSupplier( + + CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), + + ); + + const { router } = await builder.build(); + ``` + +### Patch Changes + +- 753a20c89e: Add kubernetes namespace annotation `backstage.io/kubernetes-namespace` to allow namespaced Kubernetes resources fetches +- Updated dependencies + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-kubernetes-common@0.2.9 + +## 0.5.0-next.1 + +### Minor Changes + +- 3d45427666: **BREAKING** Custom cluster suppliers need to cache their getClusters result + + To allow custom `KubernetesClustersSupplier` instances to refresh the list of clusters + the `getClusters` method is now called whenever the list of clusters is needed. + + Existing `KubernetesClustersSupplier` implementations will need to ensure that `getClusters` + can be called frequently and should return a cached result from `getClusters` instead. + + For example, here's a simple example of a custom supplier in `packages/backend/src/plugins/kubernetes.ts`: + + ```diff + -import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; + +import { + + ClusterDetails, + + KubernetesBuilder, + + KubernetesClustersSupplier, + +} from '@backstage/plugin-kubernetes-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + +import { Duration } from 'luxon'; + + + +export class CustomClustersSupplier implements KubernetesClustersSupplier { + + constructor(private clusterDetails: ClusterDetails[] = []) {} + + + + static create(refreshInterval: Duration) { + + const clusterSupplier = new CustomClustersSupplier(); + + // setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts + + runPeriodically( + + () => clusterSupplier.refreshClusters(), + + refreshInterval.toMillis(), + + ); + + return clusterSupplier; + + } + + + + async refreshClusters(): Promise { + + this.clusterDetails = []; // fetch from somewhere + + } + + + + async getClusters(): Promise { + + return this.clusterDetails; + + } + +} + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + - const { router } = await KubernetesBuilder.createBuilder({ + + const builder = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + - }).build(); + + }); + + builder.setClusterSupplier( + + CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), + + ); + + const { router } = await builder.build(); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + ## 0.4.14-next.0 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 691b9443e5..9eca295f96 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -245,6 +245,8 @@ export interface ObjectFetchParams { // (undocumented) labelSelector: string; // (undocumented) + namespace?: string; + // (undocumented) objectTypesToFetch: Set; // (undocumented) serviceId: string; diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index d4725e38c0..f7bcdd3973 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -16,7 +16,7 @@ This can be used to run the kubernetes plugin locally against a mock service. ## Steps 1. Start minikube -2. Get the Kubernetes master base url `kubectl cluster-info` +2. Get the Kubernetes master base URL `kubectl cluster-info` 3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml` 4. Get service account token (see below) 5. Start Backstage UI and backend diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index f5eb974431..125f03322e 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.4.14-next.0", + "version": "0.5.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-kubernetes-common": "^0.2.9-next.0", + "@backstage/plugin-kubernetes-common": "^0.2.9", "@google-cloud/container": "^3.0.0", "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", @@ -50,7 +50,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "helmet": "^5.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", @@ -60,7 +60,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/aws4": "^1.5.1", "aws-sdk-mock": "^5.2.1", "supertest": "^6.1.3" diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 6bef8e0009..783ec16904 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -202,6 +202,115 @@ describe('GkeClusterLocator', () => { parent: 'projects/some-project/locations/some-region', }); }); + it('dont filter out clusters when no label matcher provided', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + resourceLabels: { + foo: 'bar', + }, + }, + { + name: 'some-other-cluster', + endpoint: '6.7.8.9', + resourceLabels: { + something: 'other', + }, + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + { + authProvider: 'google', + name: 'some-other-cluster', + url: 'https://6.7.8.9', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); + it('filter out clusters without matching resource labels', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + resourceLabels: { + foo: 'bar', + }, + }, + { + name: 'some-other-cluster', + endpoint: '6.7.8.9', + resourceLabels: { + something: 'other', + }, + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + matchingResourceLabels: [ + { + key: 'foo', + value: 'bar', + }, + ], + }); + + const sut = GkeClusterLocator.fromConfigWithClient(config, { + listClusters: mockedListClusters, + } as any); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + authProvider: 'google', + name: 'some-cluster', + url: 'https://1.2.3.4', + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + expect(mockedListClusters).toBeCalledTimes(1); + expect(mockedListClusters).toHaveBeenCalledWith({ + parent: 'projects/some-project/locations/some-region', + }); + }); it('Handle errors gracefully', async () => { mockedListClusters.mockImplementation(() => { throw new Error('some error'); diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 0eb25c2acd..7a81b98032 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -25,12 +25,18 @@ import { KubernetesClustersSupplier, } from '../types/types'; +interface MatchResourceLabelEntry { + key: string; + value: string; +} + type GkeClusterLocatorOptions = { projectId: string; region?: string; skipTLSVerify?: boolean; skipMetricsLookup?: boolean; exposeDashboard?: boolean; + matchingResourceLabels?: MatchResourceLabelEntry[]; }; export class GkeClusterLocator implements KubernetesClustersSupplier { @@ -44,8 +50,13 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { static fromConfigWithClient( config: Config, client: container.v1.ClusterManagerClient, - refreshInterval: Duration | undefined = undefined, + refreshInterval?: Duration, ): GkeClusterLocator { + const matchingResourceLabels: MatchResourceLabelEntry[] = + config.getOptionalConfigArray('matchingResourceLabels')?.map(mrl => { + return { key: mrl.getString('key'), value: mrl.getString('value') }; + }) ?? []; + const options = { projectId: config.getString('projectId'), region: config.getOptionalString('region') ?? '-', @@ -53,6 +64,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { skipMetricsLookup: config.getOptionalBoolean('skipMetricsLookup') ?? false, exposeDashboard: config.getOptionalBoolean('exposeDashboard') ?? false, + matchingResourceLabels, }; const gkeClusterLocator = new GkeClusterLocator(options, client); if (refreshInterval) { @@ -91,6 +103,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { skipTLSVerify, skipMetricsLookup, exposeDashboard, + matchingResourceLabels, } = this.options; const request = { parent: `projects/${projectId}/locations/${region}`, @@ -98,24 +111,33 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { try { const [response] = await this.client.listClusters(request); - this.clusterDetails = (response.clusters ?? []).map(r => ({ - // TODO filter out clusters which don't have name or endpoint - name: r.name ?? 'unknown', - url: `https://${r.endpoint ?? ''}`, - authProvider: 'google', - skipTLSVerify, - skipMetricsLookup, - ...(exposeDashboard - ? { - dashboardApp: 'gke', - dashboardParameters: { - projectId, - region, - clusterName: r.name, - }, + this.clusterDetails = (response.clusters ?? []) + .filter(r => { + return matchingResourceLabels?.every(mrl => { + if (!r.resourceLabels) { + return false; } - : {}), - })); + return r.resourceLabels[mrl.key] === mrl.value; + }); + }) + .map(r => ({ + // TODO filter out clusters which don't have name or endpoint + name: r.name ?? 'unknown', + url: `https://${r.endpoint ?? ''}`, + authProvider: 'google', + skipTLSVerify, + skipMetricsLookup, + ...(exposeDashboard + ? { + dashboardApp: 'gke', + dashboardParameters: { + projectId, + region, + clusterName: r.name, + }, + } + : {}), + })); this.hasClusterDetails = true; } catch (e) { throw new ForwardedError( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 0db65d87d7..00090e57ac 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -207,6 +207,11 @@ export class KubernetesFanOutHandler { 'backstage.io/kubernetes-label-selector' ] || `backstage.io/kubernetes-id=${entityName}`; + const namespace = + requestBody.entity?.metadata?.annotations?.[ + 'backstage.io/kubernetes-namespace' + ]; + return Promise.all( clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { return this.fetcher @@ -216,19 +221,26 @@ export class KubernetesFanOutHandler { objectTypesToFetch: this.objectTypesToFetch, labelSelector, customResources: this.customResources, + namespace, }) .then(result => this.getMetricsForPods(clusterDetailsItem, result)) .then(r => this.toClusterObjects(clusterDetailsItem, r)); }), - ).then(r => ({ - items: r.filter( + ).then(this.toObjectsByEntityResponse); + } + + toObjectsByEntityResponse( + clusterObjects: ClusterObjects[], + ): ObjectsByEntityResponse { + return { + items: clusterObjects.filter( item => (item.errors !== undefined && item.errors.length >= 1) || (item.resources !== undefined && item.resources.length >= 1 && item.resources.some(fr => fr.resources.length >= 1)), ), - })); + }; } toClusterObjects( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index e1f701b75c..25a5c9312b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -42,6 +42,7 @@ describe('KubernetesFetcher', () => { jest.resetAllMocks(); clientMock = { listClusterCustomObject: jest.fn(), + listNamespacedCustomObject: jest.fn(), addInterceptor: jest.fn(), }; @@ -479,4 +480,47 @@ describe('KubernetesFetcher', () => { const expectedSelector = 'backstage.io/kubernetes-id=some-service'; expect(actualSelector).toBe(expectedSelector); }); + it('should use namespace if provided', async () => { + clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + namespace: 'some-namespace', + customResources: [], + }); + + const mockCall = clientMock.listNamespacedCustomObject.mock.calls[0]; + const namespace = mockCall[2]; + expect(namespace).toBe('some-namespace'); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index a2c3b9a6a8..1189dfa390 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -97,6 +97,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { params.labelSelector || `backstage.io/kubernetes-id=${params.serviceId}`, toFetch.objectType, + params.namespace, ).catch(this.captureKubernetesErrorsRethrowOthers.bind(this)); }); @@ -138,6 +139,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { resource: ObjectToFetch, labelSelector: string, objectType: KubernetesObjectTypes, + namespace?: string, ): Promise { const customObjects = this.kubernetesClientProvider.getCustomObjectsClient(clusterDetails); @@ -146,6 +148,26 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { requestOptions.uri = requestOptions.uri.replace('/apis//v1/', '/api/v1/'); }); + if (namespace) { + return customObjects + .listNamespacedCustomObject( + resource.group, + resource.apiVersion, + namespace, + resource.plural, + '', + false, + '', + '', + labelSelector, + ) + .then(r => { + return { + type: objectType, + resources: (r.body as any).items, + }; + }); + } return customObjects .listClusterCustomObject( resource.group, diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 2d7e2d3da7..12c4d2fb48 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -34,6 +34,7 @@ export interface ObjectFetchParams { objectTypesToFetch: Set; labelSelector: string; customResources: CustomResource[]; + namespace?: string; } // Fetches information from a kubernetes cluster using the cluster details object diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 05558a3e77..790cc9d3a7 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-kubernetes-common +## 0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + ## 0.2.9-next.0 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 573f9ae2f9..e9158e640a 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.2.9-next.0", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,11 +38,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@kubernetes/client-node": "^0.16.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "jest": { "roots": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 5278096b5a..dbf11048c7 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-kubernetes +## 0.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.6.4 + +### Patch Changes + +- 82e2ae6ff6: build(deps): bump `cronstrue` from 1.125.0 to 2.2.0 +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 1023ee6353: export kubernetes components +- 230ad0826f: Bump to using `@types/node` v16 +- f616d99f6f: Fix division by zero in currentToDeclaredResourceToPerc when pod resources weren't set +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-kubernetes-common@0.2.9 + +## 0.6.4-next.2 + +### Patch Changes + +- 82e2ae6ff6: build(deps): bump `cronstrue` from 1.125.0 to 2.2.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.6.4-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 433ba5538e..fa89134fc2 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.6.4-next.1", + "version": "0.6.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,12 +34,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/plugin-kubernetes-common": "^0.2.9-next.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-kubernetes-common": "^0.2.9", "@backstage/theme": "^0.2.15", "@kubernetes/client-node": "^0.16.0", "@material-ui/core": "^4.12.2", @@ -57,13 +57,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 3459a14592..4a836ad7b9 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-lighthouse +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 184a052db4..5b905609c9 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,13 +51,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index 76faeafd1b..ce74e8345d 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-newrelic-dashboard +## 0.1.13-next.0 + +### Patch Changes + +- 5fa2fb2553: Add `FetchApi` and related `fetchApiRef` which implement fetch. in order to included the Authorization Token to New Relic Dashboard Plugin, so that frontend can send an authenticated proxy call to backend +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.12-next.0 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index d94d4ff812..0400befd42 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.1.12-next.0", + "version": "0.1.13-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,19 +23,19 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/dev-utils": "^1.0.1-next.0", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5" diff --git a/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts index 2411102712..63b33056e1 100644 --- a/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts +++ b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts @@ -18,7 +18,7 @@ import { DashboardSnapshotSummary, NewRelicDashboardApi, } from './NewRelicDashboardApi'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { DashboardEntity } from '../types/DashboardEntity'; import { DashboardSnapshot } from '../types/DashboardSnapshot'; import { getDashboardParentGuidQuery } from '../queries/getDashboardParentGuidQuery'; @@ -27,13 +27,18 @@ import { ResponseError } from '@backstage/errors'; export class NewRelicDashboardClient implements NewRelicDashboardApi { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + constructor({ discoveryApi, + fetchApi, }: { discoveryApi: DiscoveryApi; + fetchApi: FetchApi; baseUrl?: string; }) { this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; } private async callApi( @@ -56,7 +61,7 @@ export class NewRelicDashboardClient implements NewRelicDashboardApi { const apiUrl = `${await this.discoveryApi.getBaseUrl( 'proxy', )}/newrelic/api/graphql`; - const response = await fetch(apiUrl, requestOptions); + const response = await this.fetchApi.fetch(apiUrl, requestOptions); if (response.status === 200) { return (await response.json()) as T; } diff --git a/plugins/newrelic-dashboard/src/plugin.ts b/plugins/newrelic-dashboard/src/plugin.ts index 9bfecff18f..d4ed11573f 100644 --- a/plugins/newrelic-dashboard/src/plugin.ts +++ b/plugins/newrelic-dashboard/src/plugin.ts @@ -19,6 +19,7 @@ import { createApiFactory, discoveryApiRef, createComponentExtension, + fetchApiRef, } from '@backstage/core-plugin-api'; import { newRelicDashboardApiRef, NewRelicDashboardClient } from './api'; import { rootRouteRef } from './routes'; @@ -31,10 +32,15 @@ export const newRelicDashboardPlugin = createPlugin({ apis: [ createApiFactory({ api: newRelicDashboardApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, fetchApi }) => new NewRelicDashboardClient({ discoveryApi, + fetchApi, baseUrl: configApi.getOptionalString('newrelicdashboard.baseUrl'), }), }), diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 72d9b05d0a..4c301dbd27 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-newrelic +## 0.3.22 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.22-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.3.22-next.0 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index d5f1b098b7..3b2d0fdf20 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.22-next.0", + "version": "0.3.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index eefd9819b0..69ba25ee8e 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-org +## 0.5.5-next.0 + +### Patch Changes + +- 8da4a207dd: Fix ref to filtered catalog table view when using aggregated relations. +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.5.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- cb592bfce7: Provides the ability to hide the relations toggle on the `OwnershipCard` as well as setting a default relation type. + + To hide the toggle simply include the `hideRelationsToggle` prop like this: + + ```tsx + + ``` + + To set the default relation type, add the `relationsType` prop with a value of direct or aggregated, the default if not provided is direct. Here is an example: + + ```tsx + + ``` + +- d014fe2cb4: Introduced a new MyGroupsSidebarItem SidebarItem that links to one or more groups based on the logged in user's membership. + + To use MyGroupsSidebarItem you'll need to add it to your `Root.tsx` like this: + + ```diff + // app/src/components/Root/Root.tsx + + import { MyGroupsSidebarItem } from '@backstage/plugin-org'; + + import GroupIcon from '@material-ui/icons/People'; + + + + //... + }> + {/* Global nav, not org-specific */} + //... + + + + //... + + + + ``` + +- 111995470d: add aggregated ownership type for kind group in OwnershipCard +- 230ad0826f: Bump to using `@types/node` v16 +- 0bada4fc4d: Added the `metadata.description` to the bottom of each member on the MembersListCard +- 99063c39ae: Minor API report cleanup +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.5.4-next.3 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.5.4-next.2 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 1528530eef..de8a699f48 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.5.4-next.2", + "version": "0.5.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,11 +48,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/catalog-client": "^1.0.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 298c921069..16a8898f0a 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -246,7 +246,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D%5B0%5D=my-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', ); }); @@ -265,6 +265,10 @@ describe('OwnershipCard', () => { type: 'memberOf', targetRef: 'group:default/my-team', }, + { + type: 'memberOf', + targetRef: 'group:custom/some-team', + }, ], }; const catalogApi: jest.Mocked = { @@ -288,7 +292,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D%5B0%5D=the-user&filters%5Bowners%5D%5B1%5D=my-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all', ); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 48760002fa..7c0bb76e2e 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -18,12 +18,14 @@ import { Entity, RELATION_MEMBER_OF, RELATION_PARENT_OF, + parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef, getEntityRelations, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import limiterFactory from 'p-limit'; import { useApi } from '@backstage/core-plugin-api'; @@ -43,18 +45,16 @@ const getQueryParams = ( selectedEntity: EntityTypeProps, ): string => { const { kind, type } = selectedEntity; - const owners = ownersEntityRef.map(owner => owner.split('/')[1]); + const owners = ownersEntityRef.map(owner => + humanizeEntityRef(parseEntityRef(owner), { defaultKind: 'group' }), + ); const filters = { kind, type, owners, user: 'all', }; - const queryParams = qs.stringify({ - filters, - }); - - return queryParams; + return qs.stringify({ filters }, { arrayFormat: 'repeat' }); }; const getOwnersEntityRef = (owner: Entity): string[] => { diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx index 794dd23747..d290001eb8 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx @@ -58,9 +58,7 @@ describe('MyGroupsSidebarItem Test', () => { }, }, ); - expect( - rendered.getByText('Mounted at /catalog/:namespace/:kind/:name'), - ).toBeInTheDocument(); + expect(rendered.container).toBeEmptyDOMElement(); }); }); diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 70ce7e5958..83a69ade78 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-pagerduty +## 0.3.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.31 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.31-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.31-next.0 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index e2967afc45..f9b70fb4b9 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.3.31-next.0", + "version": "0.3.32-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index cd6ea633b8..0370c59212 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-periskop-backend +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 382e8d2643..335546a6aa 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.2-next.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,7 +24,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@types/express": "*", "cross-fetch": "^3.0.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.6" diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index 06075ffdbe..10d18c3444 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-periskop +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.1.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.1.2-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index e37a14d7ec..985bcff656 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.2-next.0", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,11 +25,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 5bebcc8660..c73248ac4e 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-permission-backend +## 0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + +## 0.5.6 + +### Patch Changes + +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 95284162d6: - Add more specific check for policies which return conditional decisions for non-resource permissions. + - Refine permission validation in authorize endpoint to differentiate between `BasicPermission` and `ResourcePermission` instances. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + ## 0.5.6-next.1 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 32d2a3872f..404f287669 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.6-next.1", + "version": "0.5.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-node": "^0.6.0-next.1", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-node": "^0.6.1-next.0", "@types/express": "*", "dataloader": "^2.0.0", "express": "^4.17.1", @@ -39,7 +39,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index 48ab78d25c..99c996d2c0 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-permission-common +## 0.6.0 + +### Minor Changes + +- 8012ac46a0: Add `resourceType` property to `PermissionCondition` type to allow matching them with `ResourcePermission` instances. +- c98d271466: Refactor api types into more specific, decoupled names. + + - **BREAKING:** + - Renamed `AuthorizeDecision` to `EvaluatePermissionResponse` + - Renamed `AuthorizeQuery` to `EvaluatePermissionRequest` + - Renamed `AuthorizeRequest` to `EvaluatePermissionRequestBatch` + - Renamed `AuthorizeResponse` to `EvaluatePermissionResponseBatch` + - Renamed `Identified` to `IdentifiedPermissionMessage` + - Add `PermissionMessageBatch` helper type + - Add `ConditionalPolicyDecision`, `DefinitivePolicyDecision`, and `PolicyDecision` types from `@backstage/plugin-permission-node` + +### Patch Changes + +- 90754d4fa9: Removed [strict](https://github.com/colinhacks/zod#strict) validation from `PermissionCriteria` schemas to support backward-compatible changes. +- 2b07063d77: Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: + + - `authorize()`: Used when the caller requires a definitive decision. + - `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. + +- 8012ac46a0: Add `isPermission` helper method. +- 95284162d6: - Add more specific `Permission` types. + - Add `createPermission` helper to infer the appropriate type for some permission input. + - Add `isResourcePermission` helper to refine Permissions to ResourcePermissions. + +## 0.6.0-next.1 + +### Patch Changes + +- 2b07063d77: Added `PermissionEvaluator`, which will replace the existing `PermissionAuthorizer` interface. This new interface provides stronger type safety and validation by splitting `PermissionAuthorizer.authorize()` into two methods: + + - `authorize()`: Used when the caller requires a definitive decision. + - `authorizeConditional()`: Used when the caller can optimize the evaluation of any conditional decisions. For example, a plugin backend may want to use conditions in a database query instead of evaluating each resource in memory. + ## 0.6.0-next.0 ### Minor Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 4d979bf7b0..43084f9ab9 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.6.0-next.0", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -48,7 +48,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index 223209c69f..9a7bb0bba4 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -43,18 +43,9 @@ const permissionCriteriaSchema: z.ZodSchema< resourceType: z.string(), params: z.array(z.unknown()), }) - .strict() - .or( - z - .object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }) - .strict(), - ) - .or( - z - .object({ allOf: z.array(permissionCriteriaSchema).nonempty() }) - .strict(), - ) - .or(z.object({ not: permissionCriteriaSchema }).strict()), + .or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() })) + .or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() })) + .or(z.object({ not: permissionCriteriaSchema })), ); const authorizePermissionResponseSchema: z.ZodSchema = diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index a628313003..25973b1d52 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,105 @@ # @backstage/plugin-permission-node +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + +## 0.6.0 + +### Minor Changes + +- 8012ac46a0: **BREAKING**: Stronger typing in `PermissionPolicy` 🎉. + + Previously, it was entirely the responsibility of the `PermissionPolicy` author to only return `CONDITIONAL` decisions for permissions that are associated with a resource, and to return the correct kind of `PermissionCondition` instances inside the decision. Now, the policy authoring helpers provided in this package now ensure that the decision and permission match. + + **For policy authors**: rename and adjust api of `createConditionExports`. Previously, the function returned a factory for creating conditional decisions named `createPolicyDecision`, which had a couple of drawbacks: + + 1. The function always creates a _conditional_ policy decision, but this was not reflected in the name. + 2. Conditional decisions should only ever be returned from `PermissionPolicy#handle` for resource permissions, but there was nothing in the API that encoded this constraint. + + This change addresses the drawbacks above by making the following changes for policy authors: + + - The `createPolicyDecision` method has been renamed to `createConditionalDecision`. + - Along with conditions, the method now accepts a permission, which must be a `ResourcePermission`. This is expected to be the handled permission in `PermissionPolicy#handle`, whose type must first be narrowed using methods like `isPermission` and `isResourcePermission`: + + ```typescript + class TestPermissionPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + _user?: BackstageIdentityResponse, + ): Promise { + if ( + // Narrow type of `request.permission` to `ResourcePermission<'catalog-entity'> + isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY) + ) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner( + _user?.identity.ownershipEntityRefs ?? [], + ), + ); + } + + return { + result: AuthorizeResult.ALLOW, + }; + ``` + + **BREAKING**: when creating `PermissionRule`s, provide a `resourceType`. + + ```diff + export const isEntityOwner = createCatalogPermissionRule({ + name: 'IS_ENTITY_OWNER', + description: 'Allow entities owned by the current user', + + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + apply: (resource: Entity, claims: string[]) => { + if (!resource.relations) { + return false; + } + + return resource.relations + .filter(relation => relation.type === RELATION_OWNED_BY) + .some(relation => claims.includes(relation.targetRef)); + }, + toQuery: (claims: string[]) => ({ + key: 'relations.ownedBy', + values: claims, + }), + }); + ``` + +- c98d271466: **BREAKING:** + + - Rename `PolicyAuthorizeQuery` to `PolicyQuery` + - Remove `PolicyDecision`, `DefinitivePolicyDecision`, and `ConditionalPolicyDecision`. These types are now exported from `@backstage/plugin-permission-common` + +- 322b69e46a: **BREAKING:** `ServerPermissionClient` now implements `PermissionEvaluator`, which moves out the capabilities for evaluating conditional decisions from `authorize()` to `authorizeConditional()` method. + +### Patch Changes + +- 90754d4fa9: Removed [strict](https://github.com/colinhacks/zod#strict) validation from `PermissionCriteria` schemas to support backward-compatible changes. +- 8012ac46a0: Fix signature of permission rule in test suites +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/backend-common@0.13.2 + +## 0.6.0-next.2 + +### Minor Changes + +- 322b69e46a: **BREAKING:** `ServerPermissionClient` now implements `PermissionEvaluator`, which moves out the capabilities for evaluating conditional decisions from `authorize()` to `authorizeConditional()` method. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/backend-common@0.13.2-next.2 + ## 0.6.0-next.1 ### Minor Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 8bfa37b6a2..d9dfec1859 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.6.0-next.1", + "version": "0.6.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,18 +33,18 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/plugin-permission-common": "^0.6.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index b4cf4ac08c..4bdb4397dd 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -38,16 +38,14 @@ const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria > = z.lazy(() => z.union([ - z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }).strict(), - z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }).strict(), - z.object({ not: permissionCriteriaSchema }).strict(), - z - .object({ - rule: z.string(), - resourceType: z.string(), - params: z.array(z.unknown()), - }) - .strict(), + z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }), + z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }), + z.object({ not: permissionCriteriaSchema }), + z.object({ + rule: z.string(), + resourceType: z.string(), + params: z.array(z.unknown()), + }), ]), ); diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 5617640d9b..ce2f425a0b 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-permission-react +## 0.4.0 + +### Minor Changes + +- 5bdcb8c45d: **BREAKING**: More restrictive typing for `usePermission` hook and `PermissionedRoute` component. It's no longer possible to pass a `resourceRef` unless the permission is of type `ResourcePermission`. + +### Patch Changes + +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- 322b69e46a: **BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/core-plugin-api@1.0.1 + +## 0.4.0-next.1 + +### Patch Changes + +- 322b69e46a: **BREAKING:** Make `IdentityPermissionApi#authorize` typing more strict, using `AuthorizePermissionRequest` and `AuthorizePermissionResponse`. +- Updated dependencies + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-permission-common@0.6.0-next.1 + ## 0.4.0-next.0 ### Minor Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 7a846f94eb..6c8f75fb98 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.0-next.0", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0", "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@types/jest": "^26.0.7" diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 13bc583afa..27a8fc1df1 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-proxy-backend +## 0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.2.25 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.2.25-next.0 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 603a411c33..47b466e0fc 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.25-next.0", + "version": "0.2.26-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -46,7 +46,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index b39d76c6d2..a44cf6e1a6 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-rollbar-backend +## 0.1.29-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.1.28-next.0 ### Patch Changes diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md index 2dcf6495dd..d28ee60181 100644 --- a/plugins/rollbar-backend/README.md +++ b/plugins/rollbar-backend/README.md @@ -4,6 +4,58 @@ Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API. ## Setup +1. Install the plugin using: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-rollbar-backend +``` + +2. Create a `rollbar.ts` file inside `packages/backend/src/plugins/`: + +```typescript +import { createRouter } from '@backstage/plugin-rollbar-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + config: env.config, + }); +} +``` + +3. Modify your `packages/backend/src/index.ts` to include: + +```diff + ... + + import { Config } from '@backstage/config'; + import app from './plugins/app'; ++import rollbar from './plugins/rollbar'; + import scaffolder from './plugins/scaffolder'; + + ... + + async function main() { + + ... + + const authEnv = useHotMemoize(module, () => createEnv('auth')); ++ const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + + ... + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); ++ apiRouter.use('/rollbar', await rollbar(rollbarEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); +``` + The following values are read from the configuration file. ```yaml diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 24fb97af92..b93723a286 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.28-next.0", + "version": "0.1.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@types/express": "^4.17.6", "camelcase-keys": "^7.0.1", @@ -42,7 +42,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "lodash": "^4.17.21", "morgan": "^1.10.0", "node-fetch": "^2.6.7", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "msw": "^0.36.3", "supertest": "^6.1.3" diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 4295d86daa..c1d7776dd5 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-rollbar +## 0.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.4.4 + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.4.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 81fca1d7c7..217e2d4041 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.4-next.0", + "version": "0.4.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,13 +53,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 2472364736..fdf79fd08f 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.7-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/integration@1.2.0-next.0 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-common@0.13.2 + ## 0.2.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index ba056e6bc6..b196b4da99 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.6-next.1", + "version": "0.2.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,19 +23,19 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-scaffolder-backend": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-scaffolder-backend": "^1.2.0-next.0", "@backstage/config": "^1.0.0", "@backstage/types": "^1.0.0", "command-exists": "^1.2.9", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 9d7b814a5c..3057cd94dc 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + - @backstage/integration@1.2.0-next.0 + +## 0.3.6 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/backend-common@0.13.2 + +## 0.3.6-next.2 + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + - @backstage/integration@1.1.0-next.2 + ## 0.3.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index d1e100720d..226d4c0420 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.3.6-next.1", + "version": "0.3.7-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,17 +24,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/plugin-scaffolder-backend": "^1.1.0-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/plugin-scaffolder-backend": "^1.2.0-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", + "@backstage/integration": "^1.2.0-next.0", "@backstage/types": "^1.0.0", "command-exists": "^1.2.9", "fs-extra": "^10.0.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 8d0313bd1d..bc7c0f5b55 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.2.0-next.0 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.1.0 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 948acc97fc..10371e89af 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.4-next.1", + "version": "0.2.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/plugin-scaffolder-backend": "^1.1.0-next.1", + "@backstage/plugin-scaffolder-backend": "^1.2.0-next.0", "@backstage/types": "^1.0.0", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" }, "devDependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 23496051d2..bf4a9718f0 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-scaffolder-backend +## 1.2.0-next.0 + +### Minor Changes + +- 9818112d12: Update the `github:publish` action to allow passing required status check + contexts before merging to the main branch. +- 8d5a2238a9: Split `publish:bitbucket` into `publish:bitbucketCloud` and `publish:bitbucketServer`. + + In order to migrate from the deprecated action, you need to replace the use of action + `publish:bitbucket` in your templates with the use of either `publish:bitbucketCloud` + or `publish:bitbucketServer` - depending on which destination SCM provider you use. + + Additionally, these actions will not utilize `integrations.bitbucket` anymore, + but `integrations.bitbucketCloud` or `integrations.bitbucketServer` respectively. + You may or may not have migrated to these already. + + As described in a previous changeset, using these two replacement integrations configs + will not compromise use cases which still rely on `integrations.bitbucket` as this was + set up in a backwards compatible way. + + Additionally, please mind that the option `enableLFS` is only available (and always was) + for Bitbucket Server use cases and therefore, is not even part of the schema for + `publish:bitbucketCloud` anymore. + +### Patch Changes + +- 0fc65cbf89: Override default commit message and author details in GitHub, Azure, bitbucket +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.1.2-next.0 + +## 1.1.0 + +### Minor Changes + +- 2a7d52ca2c: Override default commit message and author details in GitLab action +- f5f921dafb: Add new `draft` option to the `publish:github:pull-request` action. + +### Patch Changes + +- 64d9a031a8: build(deps): bump `isbinaryfile` from 4.0.8 to 5.0.0 +- 2f3d3a1eae: build(deps): bump `@gitbeaker/core` from 34.6.0 to 35.6.0 +- Updated dependencies + - @backstage/plugin-catalog-backend@1.1.0 + - @backstage/integration@1.1.0 + - @backstage/catalog-model@1.0.1 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + ## 1.1.0-next.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 2c44004e5b..1bc8a27f54 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -193,10 +193,43 @@ export function createPublishAzureAction(options: { defaultBranch?: string | undefined; sourcePath?: string | undefined; token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; + +// @public @deprecated +export function createPublishBitbucketAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + enableLFS?: boolean | undefined; + token?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; }>; // @public -export function createPublishBitbucketAction(options: { +export function createPublishBitbucketCloudAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction<{ + repoUrl: string; + description?: string | undefined; + defaultBranch?: string | undefined; + repoVisibility?: 'private' | 'public' | undefined; + sourcePath?: string | undefined; + token?: string | undefined; +}>; + +// @public +export function createPublishBitbucketServerAction(options: { integrations: ScmIntegrationRegistry; config: Config; }): TemplateAction<{ @@ -225,11 +258,15 @@ export function createPublishGithubAction(options: { access?: string | undefined; defaultBranch?: string | undefined; deleteBranchOnMerge?: boolean | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; allowRebaseMerge?: boolean | undefined; allowSquashMerge?: boolean | undefined; allowMergeCommit?: boolean | undefined; sourcePath?: string | undefined; requireCodeOwnerReviews?: boolean | undefined; + requiredStatusCheckContexts?: string[] | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; collaborators?: | { diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index f36029c44b..d9492902d0 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.1.0-next.2", + "version": "1.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,14 +34,14 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-backend": "^1.1.0-next.2", - "@backstage/plugin-scaffolder-common": "^1.0.1-next.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-backend": "^1.1.2-next.0", + "@backstage/plugin-scaffolder-common": "^1.0.1", "@backstage/types": "^1.0.0", "@gitbeaker/core": "^35.6.0", "@gitbeaker/node": "^35.1.0", @@ -53,7 +53,7 @@ "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "git-url-parse": "^11.6.0", "globby": "^11.0.0", "isbinaryfile": "^5.0.0", @@ -74,8 +74,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.2", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 93c5991ca9..0d269019df 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -37,6 +37,8 @@ import { import { createPublishAzureAction, createPublishBitbucketAction, + createPublishBitbucketCloudAction, + createPublishBitbucketServerAction, createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, @@ -129,6 +131,14 @@ export const createBuiltinActions = ( integrations, config, }), + createPublishBitbucketCloudAction({ + integrations, + config, + }), + createPublishBitbucketServerAction({ + integrations, + config, + }), createPublishAzureAction({ integrations, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 75e22522d5..529a504cba 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -132,6 +132,7 @@ type BranchProtectionOptions = { repoName: string; logger: Logger; requireCodeOwnerReviews: boolean; + requiredStatusCheckContexts?: string[]; defaultBranch?: string; }; @@ -141,6 +142,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ owner, logger, requireCodeOwnerReviews, + requiredStatusCheckContexts = [], defaultBranch = 'master', }: BranchProtectionOptions): Promise => { const tryOnce = async () => { @@ -159,7 +161,10 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ owner, repo: repoName, branch: defaultBranch, - required_status_checks: { strict: true, contexts: [] }, + required_status_checks: { + strict: true, + contexts: requiredStatusCheckContexts, + }, restrictions: null, enforce_admins: true, required_pull_request_reviews: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 9247109121..ee901453e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -192,6 +192,7 @@ describe('publish:azure', () => { defaultBranch: 'master', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -215,6 +216,7 @@ describe('publish:azure', () => { defaultBranch: 'master', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -254,6 +256,7 @@ describe('publish:azure', () => { auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', + commitMessage: 'initial commit', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); @@ -290,7 +293,7 @@ describe('publish:azure', () => { auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', - commitMessage: 'Test commit message', + commitMessage: 'initial commit', gitAuthorInfo: { email: undefined, name: undefined }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 7a8e963b5c..447b39573c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -40,6 +40,9 @@ export function createPublishAzureAction(options: { defaultBranch?: string; sourcePath?: string; token?: string; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; }>({ id: 'publish:azure', description: @@ -62,6 +65,21 @@ export function createPublishAzureAction(options: { type: 'string', description: `Sets the default branch on the repository. The default value is 'master'`, }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, sourcePath: { title: 'Source Path', description: @@ -90,7 +108,13 @@ export function createPublishAzureAction(options: { }, }, async handler(ctx) { - const { repoUrl, defaultBranch = 'master' } = ctx.input; + const { + repoUrl, + defaultBranch = 'master', + gitCommitMessage = 'initial commit', + gitAuthorName, + gitAuthorEmail, + } = ctx.input; const { owner, repo, host, organization } = parseRepoUrl( repoUrl, @@ -142,8 +166,12 @@ export function createPublishAzureAction(options: { const repoContentsUrl = remoteUrl; const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.defaultAuthor.name'), - email: config.getOptionalString('scaffolder.defaultAuthor.email'), + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ @@ -155,9 +183,9 @@ export function createPublishAzureAction(options: { password: token, }, logger: ctx.logger, - commitMessage: config.getOptionalString( - 'scaffolder.defaultCommitMessage', - ), + commitMessage: gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'), gitAuthorInfo, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index ee989038c2..926910467d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -348,6 +348,7 @@ describe('publish:bitbucket', () => { defaultBranch: 'master', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -391,6 +392,7 @@ describe('publish:bitbucket', () => { defaultBranch: 'main', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -460,6 +462,7 @@ describe('publish:bitbucket', () => { auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', + commitMessage: 'initial commit', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); @@ -526,7 +529,7 @@ describe('publish:bitbucket', () => { auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', - commitMessage: 'Test commit message', + commitMessage: 'initial commit', gitAuthorInfo: { email: undefined, name: undefined }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 665c54fb3e..8f72157fc2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -198,6 +198,7 @@ const performEnableLFS = async (opts: { * Creates a new action that initializes a git repository of the content in the workspace * and publishes it to Bitbucket. * @public + * @deprecated in favor of createPublishBitbucketCloudAction and createPublishBitbucketServerAction */ export function createPublishBitbucketAction(options: { integrations: ScmIntegrationRegistry; @@ -213,6 +214,9 @@ export function createPublishBitbucketAction(options: { sourcePath?: string; enableLFS?: boolean; token?: string; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; }>({ id: 'publish:bitbucket', description: @@ -257,6 +261,21 @@ export function createPublishBitbucketAction(options: { type: 'string', description: 'The token to use for authorization to BitBucket', }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, }, }, output: { @@ -274,12 +293,18 @@ export function createPublishBitbucketAction(options: { }, }, async handler(ctx) { + ctx.logger.warn( + `[Deprecated] Please migrate the use of action "publish:bitbucket" to "publish:bitbucketCloud" or "publish:bitbucketServer".`, + ); const { repoUrl, description, defaultBranch = 'master', repoVisibility = 'private', enableLFS = false, + gitCommitMessage = 'initial commit', + gitAuthorName, + gitAuthorEmail, } = ctx.input; const { workspace, project, repo, host } = parseRepoUrl( @@ -340,8 +365,12 @@ export function createPublishBitbucketAction(options: { }); const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.defaultAuthor.name'), - email: config.getOptionalString('scaffolder.defaultAuthor.email'), + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), }; let auth; @@ -368,9 +397,9 @@ export function createPublishBitbucketAction(options: { auth, defaultBranch, logger: ctx.logger, - commitMessage: config.getOptionalString( - 'scaffolder.defaultCommitMessage', - ), + commitMessage: gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'), gitAuthorInfo, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts new file mode 100644 index 0000000000..b21a47f506 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts @@ -0,0 +1,480 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('../helpers'); + +import { createPublishBitbucketCloudAction } from './bitbucketCloud'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; + +describe('publish:bitbucketCloud', () => { + const config = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketCloudAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }, + workspacePath: 'wsp', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?project=project&repo=repo', + }, + }), + ).rejects.toThrow(/missing workspace/); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&repo=repo', + }, + }), + ).rejects.toThrow(/missing project/); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&project=project', + }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo', + }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('should throw if there is no token in the integration config that is returned', async () => { + const configNoCreds = new ConfigReader({ + integrations: { + bitbucketCloud: [], + }, + }); + + const integrationsNoCreds = ScmIntegrations.fromConfig(configNoCreds); + const actionNoCreds = createPublishBitbucketCloudAction({ + integrations: integrationsNoCreds, + config: configNoCreds, + }); + + await expect(actionNoCreds.handler(mockContext)).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + + it('should call the correct APIs', async () => { + expect.assertions(2); + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic dTpw'); + expect(req.body).toEqual({ + is_private: true, + scm: 'git', + project: { key: 'project' }, + }); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler(mockContext); + }); + + it('should work if the token is provided through ctx.input', async () => { + expect.assertions(2); + const token = 'user-token'; + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + expect(req.body).toEqual({ + is_private: true, + scm: 'git', + project: { key: 'project' }, + }); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/repo', + }, + ], + }, + }), + ); + }, + ), + ); + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + token: token, + }, + }); + }); + + it('should call initAndPush with the correct values', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'master', + auth: { username: 'u', password: 'p' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the correct default branch', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'main', + auth: { username: 'u', password: 'p' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createPublishBitbucketCloudAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'u', password: 'p' }, + logger: mockContext.logger, + defaultBranch: 'master', + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + + it('should call initAndPush with the configured defaultCommitMessage', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'u', + appPassword: 'p', + }, + ], + }, + scaffolder: { + defaultCommitMessage: 'Test commit message', + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createPublishBitbucketCloudAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'u', password: 'p' }, + logger: mockContext.logger, + defaultBranch: 'master', + commitMessage: 'Test commit message', + gitAuthorInfo: { email: undefined, name: undefined }, + }); + }); + + it('should call outputs with the correct urls', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.org/workspace/cloneurl', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.org/workspace/repo/src/master', + ); + }); + + it('should call outputs with the correct urls with correct default branch', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.org/workspace/cloneurl', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.org/workspace/repo/src/main', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts new file mode 100644 index 0000000000..02e29c33de --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts @@ -0,0 +1,281 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import fetch, { Response, RequestInit } from 'node-fetch'; +import { initRepoAndPush } from '../helpers'; +import { createTemplateAction } from '../../createTemplateAction'; +import { getRepoSourceDirectory, parseRepoUrl } from './util'; +import { Config } from '@backstage/config'; + +const createRepository = async (opts: { + workspace: string; + project: string; + repo: string; + description?: string; + repoVisibility: 'private' | 'public'; + mainBranch: string; + authorization: string; + apiBaseUrl: string; +}) => { + const { + workspace, + project, + repo, + description, + repoVisibility, + mainBranch, + authorization, + apiBaseUrl, + } = opts; + + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({ + scm: 'git', + description: description, + is_private: repoVisibility === 'private', + project: { key: project }, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + let response: Response; + try { + response = await fetch( + `${apiBaseUrl}/repositories/${workspace}/${repo}`, + options, + ); + } catch (e) { + throw new Error(`Unable to create repository, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to create repository, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + let remoteUrl = ''; + for (const link of r.links.clone) { + if (link.name === 'https') { + remoteUrl = link.href; + } + } + + // "mainbranch.name" cannot be set neither at create nor update of the repo + // the first pushed branch will be set as "main branch" then + const repoContentsUrl = `${r.links.html.href}/src/${mainBranch}`; + return { remoteUrl, repoContentsUrl }; +}; + +const getAuthorizationHeader = (config: { + username?: string; + appPassword?: string; + token?: string; +}) => { + if (config.username && config.appPassword) { + const buffer = Buffer.from( + `${config.username}:${config.appPassword}`, + 'utf8', + ); + + return `Basic ${buffer.toString('base64')}`; + } + + if (config.token) { + return `Bearer ${config.token}`; + } + + throw new Error( + `Authorization has not been provided for Bitbucket Cloud. Please add either username + appPassword to the Integrations config or a user login auth token`, + ); +}; + +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to Bitbucket Cloud. + * @public + */ +export function createPublishBitbucketCloudAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations, config } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + defaultBranch?: string; + repoVisibility?: 'private' | 'public'; + sourcePath?: string; + token?: string; + }>({ + id: 'publish:bitbucketCloud', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket Cloud.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + repoVisibility: { + title: 'Repository Visibility', + type: 'string', + enum: ['private', 'public'], + }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, + sourcePath: { + title: 'Source Path', + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', + type: 'string', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: + 'The token to use for authorization to BitBucket Cloud', + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + defaultBranch = 'master', + repoVisibility = 'private', + } = ctx.input; + + const { workspace, project, repo, host } = parseRepoUrl( + repoUrl, + integrations, + ); + + if (!workspace) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } + + if (!project) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + ); + } + + const integrationConfig = integrations.bitbucketCloud.byHost(host); + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const authorization = getAuthorizationHeader( + ctx.input.token ? { token: ctx.input.token } : integrationConfig.config, + ); + + const apiBaseUrl = integrationConfig.config.apiBaseUrl; + + const { remoteUrl, repoContentsUrl } = await createRepository({ + authorization, + workspace: workspace || '', + project, + repo, + repoVisibility, + mainBranch: defaultBranch, + description, + apiBaseUrl, + }); + + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + let auth; + + if (ctx.input.token) { + auth = { + username: 'x-token-auth', + password: ctx.input.token, + }; + } else { + if ( + !integrationConfig.config.username || + !integrationConfig.config.appPassword + ) { + throw new Error( + 'Credentials for Bitbucket Cloud integration required for this action.', + ); + } + + auth = { + username: integrationConfig.config.username, + password: integrationConfig.config.appPassword, + }; + } + + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + remoteUrl, + auth, + defaultBranch, + logger: ctx.logger, + commitMessage: config.getOptionalString( + 'scaffolder.defaultCommitMessage', + ), + gitAuthorInfo, + }); + + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts new file mode 100644 index 0000000000..338231ea55 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.test.ts @@ -0,0 +1,593 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('../helpers'); + +import { createPublishBitbucketServerAction } from './bitbucketServer'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; + +describe('publish:bitbucketServer', () => { + const config = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketServerAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'private' as const, + }, + workspacePath: 'wsp', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?repo=repo', + }, + }), + ).rejects.toThrow(/missing project/); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project', + }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'missing.com?project=project&repo=repo', + }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('should throw if there is no token in the integration config that is returned', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', + }, + }), + ).rejects.toThrow( + /Authorization has not been provided for notoken.bitbucket.com/, + ); + }); + + it('should call the correct APIs', async () => { + expect.assertions(2); + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + }, + }); + }); + + it('should work if the token is provided through ctx.input', async () => { + expect.assertions(2); + const token = 'user-token'; + server.use( + rest.post( + 'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', + token: token, + }, + }); + }); + + describe('LFS for hosted bitbucket', () => { + const repoCreationResponse = { + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }; + + it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => { + expect.assertions(1); + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoCreationResponse), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + return res(ctx.status(204)); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + enableLFS: true, + }, + }); + }); + + it('should report an error if enabling LFS fails', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoCreationResponse), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (_, res, ctx) => { + return res(ctx.status(500)); + }, + ), + ); + + await expect( + action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + enableLFS: true, + }, + }), + ).rejects.toThrow(/Failed to enable LFS/); + }); + }); + + it('should call initAndPush with the correct values', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + defaultBranch: 'master', + auth: { username: 'x-token-auth', password: 'thing' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the correct default branch', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + defaultBranch: 'main', + auth: { username: 'x-token-auth', password: 'thing' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the configured defaultAuthor', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + scaffolder: { + defaultAuthor: { + name: 'Test', + email: 'example@example.com', + }, + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createPublishBitbucketServerAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { username: 'x-token-auth', password: 'thing' }, + logger: mockContext.logger, + defaultBranch: 'master', + gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, + }); + }); + + it('should call initAndPush with the configured defaultCommitMessage', async () => { + const customAuthorConfig = new ConfigReader({ + integrations: { + bitbucketServer: [ + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + scaffolder: { + defaultCommitMessage: 'Test commit message', + }, + }); + + const customAuthorIntegrations = + ScmIntegrations.fromConfig(customAuthorConfig); + const customAuthorAction = createPublishBitbucketServerAction({ + integrations: customAuthorIntegrations, + config: customAuthorConfig, + }); + + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await customAuthorAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { username: 'x-token-auth', password: 'thing' }, + logger: mockContext.logger, + defaultBranch: 'master', + commitMessage: 'Test commit message', + gitAuthorInfo: { email: undefined, name: undefined }, + }); + }); + + it('should call outputs with the correct urls', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.mycompany.com/scm/project/repo', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.mycompany.com/projects/project/repos/repo', + ); + }); + + it('should call outputs with the correct urls with correct default branch', async () => { + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.mycompany.com/scm/project/repo', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.mycompany.com/projects/project/repos/repo', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts new file mode 100644 index 0000000000..208403d6f4 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketServer.ts @@ -0,0 +1,265 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import fetch, { Response, RequestInit } from 'node-fetch'; +import { initRepoAndPush } from '../helpers'; +import { createTemplateAction } from '../../createTemplateAction'; +import { getRepoSourceDirectory, parseRepoUrl } from './util'; +import { Config } from '@backstage/config'; + +const createRepository = async (opts: { + project: string; + repo: string; + description?: string; + repoVisibility: 'private' | 'public'; + authorization: string; + apiBaseUrl: string; +}) => { + const { + project, + repo, + description, + authorization, + repoVisibility, + apiBaseUrl, + } = opts; + + let response: Response; + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({ + name: repo, + description: description, + public: repoVisibility === 'public', + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options); + } catch (e) { + throw new Error(`Unable to create repository, ${e}`); + } + + if (response.status !== 201) { + throw new Error( + `Unable to create repository, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + let remoteUrl = ''; + for (const link of r.links.clone) { + if (link.name === 'http') { + remoteUrl = link.href; + } + } + + const repoContentsUrl = `${r.links.self[0].href}`; + return { remoteUrl, repoContentsUrl }; +}; + +const getAuthorizationHeader = (config: { token: string }) => { + return `Bearer ${config.token}`; +}; + +const performEnableLFS = async (opts: { + authorization: string; + host: string; + project: string; + repo: string; +}) => { + const { authorization, host, project, repo } = opts; + + const options: RequestInit = { + method: 'PUT', + headers: { + Authorization: authorization, + }, + }; + + const { ok, status, statusText } = await fetch( + `https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`, + options, + ); + + if (!ok) + throw new Error( + `Failed to enable LFS in the repository, ${status}: ${statusText}`, + ); +}; + +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to Bitbucket Server. + * @public + */ +export function createPublishBitbucketServerAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations, config } = options; + + return createTemplateAction<{ + repoUrl: string; + description?: string; + defaultBranch?: string; + repoVisibility?: 'private' | 'public'; + sourcePath?: string; + enableLFS?: boolean; + token?: string; + }>({ + id: 'publish:bitbucketServer', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket Server.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + repoVisibility: { + title: 'Repository Visibility', + type: 'string', + enum: ['private', 'public'], + }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, + sourcePath: { + title: 'Source Path', + description: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', + type: 'string', + }, + enableLFS: { + title: 'Enable LFS?', + description: 'Enable LFS for the repository.', + type: 'boolean', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: + 'The token to use for authorization to BitBucket Server', + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + defaultBranch = 'master', + repoVisibility = 'private', + enableLFS = false, + } = ctx.input; + + const { project, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!project) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + ); + } + + const integrationConfig = integrations.bitbucketServer.byHost(host); + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const token = ctx.input.token ?? integrationConfig.config.token; + if (!token) { + throw new Error( + `Authorization has not been provided for ${integrationConfig.config.host}. Please add either token to the Integrations config or a user login auth token`, + ); + } + + const authorization = getAuthorizationHeader({ token }); + + const apiBaseUrl = integrationConfig.config.apiBaseUrl; + + const { remoteUrl, repoContentsUrl } = await createRepository({ + authorization, + project, + repo, + repoVisibility, + description, + apiBaseUrl, + }); + + const gitAuthorInfo = { + name: config.getOptionalString('scaffolder.defaultAuthor.name'), + email: config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + const auth = { + username: 'x-token-auth', + password: token, + }; + + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + remoteUrl, + auth, + defaultBranch, + logger: ctx.logger, + commitMessage: config.getOptionalString( + 'scaffolder.defaultCommitMessage', + ), + gitAuthorInfo, + }); + + if (enableLFS) { + await performEnableLFS({ authorization, host, project, repo }); + } + + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 4e835657a3..f1a6c9bfe8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -198,6 +198,7 @@ describe('publish:github', () => { defaultBranch: 'master', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -228,6 +229,7 @@ describe('publish:github', () => { defaultBranch: 'main', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: {}, }); }); @@ -275,6 +277,7 @@ describe('publish:github', () => { defaultBranch: 'master', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, + commitMessage: 'initial commit', gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, }); }); @@ -319,7 +322,7 @@ describe('publish:github', () => { defaultBranch: 'master', auth: { username: 'x-access-token', password: 'tokenlols' }, logger: mockContext.logger, - commitMessage: 'Test commit message', + commitMessage: 'initial commit', gitAuthorInfo: { email: undefined, name: undefined }, }); }); @@ -625,6 +628,7 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], }); await action.handler({ @@ -642,6 +646,7 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: true, + requiredStatusCheckContexts: [], }); await action.handler({ @@ -659,6 +664,67 @@ describe('publish:github', () => { logger: mockContext.logger, defaultBranch: 'master', requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredStatusCheckContexts', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repository', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: ['statusCheck'], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: ['statusCheck'], + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requiredStatusCheckContexts: [], + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 7f1e7a1c93..340eb06dcd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -47,11 +47,15 @@ export function createPublishGithubAction(options: { access?: string; defaultBranch?: string; deleteBranchOnMerge?: boolean; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; allowRebaseMerge?: boolean; allowSquashMerge?: boolean; allowMergeCommit?: boolean; sourcePath?: string; requireCodeOwnerReviews?: boolean; + requiredStatusCheckContexts?: string[]; repoVisibility?: 'private' | 'internal' | 'public'; collaborators?: Array<{ username: string; @@ -88,6 +92,15 @@ export function createPublishGithubAction(options: { 'Require an approved review in PR including files with a designated Code Owner', type: 'boolean', }, + requiredStatusCheckContexts: { + title: 'Required Status Check Contexts', + description: + 'The list of status checks to require in order to merge into this branch', + type: 'array', + items: { + type: 'string', + }, + }, repoVisibility: { title: 'Repository Visibility', type: 'string', @@ -103,6 +116,21 @@ export function createPublishGithubAction(options: { type: 'boolean', description: `Delete the branch after merging the PR. The default value is 'false'`, }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, allowMergeCommit: { title: 'Allow Merge Commits', type: 'boolean', @@ -178,9 +206,13 @@ export function createPublishGithubAction(options: { description, access, requireCodeOwnerReviews = false, + requiredStatusCheckContexts = [], repoVisibility = 'private', defaultBranch = 'master', deleteBranchOnMerge = false, + gitCommitMessage = 'initial commit', + gitAuthorName, + gitAuthorEmail, allowMergeCommit = true, allowSquashMerge = true, allowRebaseMerge = true, @@ -290,8 +322,12 @@ export function createPublishGithubAction(options: { const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`; const gitAuthorInfo = { - name: config.getOptionalString('scaffolder.defaultAuthor.name'), - email: config.getOptionalString('scaffolder.defaultAuthor.email'), + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), }; await initRepoAndPush({ @@ -303,9 +339,9 @@ export function createPublishGithubAction(options: { password: octokitOptions.auth, }, logger: ctx.logger, - commitMessage: config.getOptionalString( - 'scaffolder.defaultCommitMessage', - ), + commitMessage: gitCommitMessage + ? gitCommitMessage + : config.getOptionalString('scaffolder.defaultCommitMessage'), gitAuthorInfo, }); @@ -317,6 +353,7 @@ export function createPublishGithubAction(options: { logger: ctx.logger, defaultBranch, requireCodeOwnerReviews, + requiredStatusCheckContexts, }); } catch (e) { assertError(e); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index 7d85b45edd..02266b6a06 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -16,6 +16,8 @@ export { createPublishAzureAction } from './azure'; export { createPublishBitbucketAction } from './bitbucket'; +export { createPublishBitbucketCloudAction } from './bitbucketCloud'; +export { createPublishBitbucketServerAction } from './bitbucketServer'; export { createPublishFileAction } from './file'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 52599fe72a..a1b2d4511d 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-scaffolder-common +## 1.0.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.0.1 + ## 1.0.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index a9227ab790..1f3cad0f4a 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.0.1-next.0", + "version": "1.0.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,10 +39,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/types": "^1.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 7e844250d6..fd22cdf2b5 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-scaffolder +## 1.2.0-next.0 + +### Minor Changes + +- 06ab5218f9: Add new bitbucket(Cloud/Server) integrations to ScaffolderClient. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + +## 1.1.0 + +### Minor Changes + +- 6331ec1ebc: Added a context menu to the scaffolder page that provides links to the template editor and actions reference. These links and the presence of the context menu can be toggled through the `contextMenu` prop of the scaffolder page. +- 8fad3b6ef5: The Template Preview page has been renamed to Template Editor, and is not available at the `/edit` path instead. There is a redirect in place from `/preview`. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 946af407db: Changed input label for owner field in GitlabRepoPicker +- 230ad0826f: Bump to using `@types/node` v16 +- d34900af81: Added a new `NextScaffolderRouter` which will eventually replace the exiting router +- 9975ff9852: Applied the fix from version 1.0.1 of this package, which is part of the v1.0.2 release of Backstage. +- 4431873583: Update `usePermission` usage. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-react@0.4.0 + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/integration-react@1.0.1 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/catalog-client@1.0.1 + - @backstage/plugin-scaffolder-common@1.0.1 + +## 1.1.0-next.3 + +### Minor Changes + +- 6331ec1ebc: Added a context menu to the scaffolder page that provides links to the template editor and actions reference. These links and the presence of the context menu can be toggled through the `contextMenu` prop of the scaffolder page. +- 8fad3b6ef5: The Template Preview page has been renamed to Template Editor, and is not available at the `/edit` path instead. There is a redirect in place from `/preview`. + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 946af407db: Changed input label for owner field in GitlabRepoPicker +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/integration-react@1.0.1-next.2 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-permission-react@0.4.0-next.1 + - @backstage/integration@1.1.0-next.2 + ## 1.0.1-next.2 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 0ef620776b..ecf900203b 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.0.1-next.2", + "version": "1.2.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,30 +35,30 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/integration-react": "^1.0.1-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", - "@backstage/plugin-permission-react": "^0.4.0-next.0", - "@backstage/plugin-scaffolder-common": "^1.0.1-next.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-permission-react": "^0.4.0", + "@backstage/plugin-scaffolder-common": "^1.0.1", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", - "@codemirror/legacy-modes": "^0.19.0", - "@codemirror/panel": "^0.19.1", - "@codemirror/stream-parser": "^0.19.6", + "@codemirror/legacy-modes": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/view": "^0.20.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", "@rjsf/material-ui": "^3.2.1", "@types/json-schema": "^7.0.9", - "@uiw/react-codemirror": "^4.5.1", + "@uiw/react-codemirror": "^4.7.0", "classnames": "^2.2.6", "git-url-parse": "^11.6.0", "humanize-duration": "^3.25.1", @@ -79,14 +79,14 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/plugin-catalog": "^1.1.0-next.2", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/humanize-duration": "^3.18.1", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 818ee7971b..6fc456e1fe 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -75,7 +75,15 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise { const integrations = [ ...this.scmIntegrationsApi.azure.list(), - ...this.scmIntegrationsApi.bitbucket.list(), + ...this.scmIntegrationsApi.bitbucket + .list() + .filter( + item => + !this.scmIntegrationsApi.bitbucketCloud.byHost(item.config.host) && + !this.scmIntegrationsApi.bitbucketServer.byHost(item.config.host), + ), + ...this.scmIntegrationsApi.bitbucketCloud.list(), + ...this.scmIntegrationsApi.bitbucketServer.list(), ...this.scmIntegrationsApi.github.list(), ...this.scmIntegrationsApi.gitlab.list(), ] diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index aa04e8d1c6..71c64a9fa4 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -25,8 +25,9 @@ import { } from '@backstage/plugin-catalog-react'; import { JsonObject } from '@backstage/types'; import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; -import { showPanel } from '@codemirror/panel'; -import { StreamLanguage } from '@codemirror/stream-parser'; +import { showPanel } from '@codemirror/view'; +import { StreamLanguage } from '@codemirror/language'; + import { FormControl, Grid, diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index de7cd608d8..a328c09098 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.1.4-next.0 + +### Patch Changes + +- a7f7a63d14: Prevent orphaned stale indices by permanently marking them for deletion so removal can be re-attempted if it failed previously +- Updated dependencies + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## 0.1.3 + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + - @backstage/plugin-search-backend-node@0.6.0 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@0.6.0-next.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index cb8eca0c18..f53b20e377 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.1.3-next.0", + "version": "0.1.4-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/plugin-search-backend-node": "^0.5.3-next.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/plugin-search-backend-node": "^0.6.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", @@ -34,8 +34,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/cli": "^0.17.0-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/cli": "^0.17.1-next.0", "@elastic/elasticsearch-mock": "^1.0.0" }, "files": [ diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts index 0867887d9a..4e0e8e229f 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -73,11 +73,19 @@ describe('ElasticSearchSearchEngineIndexer', () => { 'routing.search': '-', is_write_index: '-', }, + { + alias: 'some-type-index__search_removable', + index: 'some-type-index__456tobedeleted', + filter: '-', + 'routing.index': '-', + 'routing.search': '-', + is_write_index: '-', + }, ]); mock.add( { method: 'GET', - path: '/_cat/aliases/some-type-index__search', + path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable', }, catSpy, ); @@ -108,7 +116,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { mock.add( { method: 'DELETE', - path: '/some-type-index__123tobedeleted', + path: '/some-type-index__123tobedeleted%2Csome-type-index__456tobedeleted', }, deleteSpy, ); @@ -151,6 +159,15 @@ describe('ElasticSearchSearchEngineIndexer', () => { remove: { index: 'some-type-index__*', alias: 'some-type-index__search' }, }); expect(aliasActions[1]).toStrictEqual({ + add: { + indices: [ + 'some-type-index__123tobedeleted', + 'some-type-index__456tobedeleted', + ], + alias: 'some-type-index__search_removable', + }, + }); + expect(aliasActions[2]).toStrictEqual({ add: { index: createdIndex, alias: 'some-type-index__search' }, }); @@ -193,12 +210,12 @@ describe('ElasticSearchSearchEngineIndexer', () => { catSpy = jest.fn().mockReturnValue([]); mock.clear({ method: 'GET', - path: '/_cat/aliases/some-type-index__search', + path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable', }); mock.add( { method: 'GET', - path: '/_cat/aliases/some-type-index__search', + path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable', }, catSpy, ); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index cb3674ddd1..bf07b8a36d 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -46,6 +46,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { private readonly indexPrefix: string; private readonly indexSeparator: string; private readonly alias: string; + private readonly removableAlias: string; private readonly logger: Logger; private readonly sourceStream: Readable; private readonly elasticSearchClient: Client; @@ -60,6 +61,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { this.indexSeparator = options.indexSeparator; this.indexName = this.constructIndexName(`${Date.now()}`); this.alias = options.alias; + this.removableAlias = `${this.alias}_removable`; this.elasticSearchClient = options.elasticSearchClient; // The ES client bulk helper supports stream-based indexing, but we have to @@ -90,12 +92,12 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { const aliases = await this.elasticSearchClient.cat.aliases({ format: 'json', - name: this.alias, + name: [this.alias, this.removableAlias], }); - this.removableIndices = aliases.body.map( - (r: Record) => r.index, - ); + this.removableIndices = [ + ...new Set(aliases.body.map((r: Record) => r.index)), + ] as string[]; await this.elasticSearchClient.indices.create({ index: this.indexName, @@ -121,8 +123,9 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { // Wait for the bulk helper to finish processing. const result = await this.bulkResult; - // Rotate aliases upon completion. Allow errors to bubble up so that we can - // clean up the create index. + // Rotate main alias upon completion. Apply permanent secondary alias so + // stale indices can be referenced for deletion in case initial attempt + // fails. Allow errors to bubble up so that we can clean up the created index. this.logger.info( `Indexing completed for index ${this.type} in ${duration( this.startTimestamp, @@ -135,8 +138,18 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { { remove: { index: this.constructIndexName('*'), alias: this.alias }, }, - { add: { index: this.indexName, alias: this.alias } }, - ], + this.removableIndices.length + ? { + add: { + indices: this.removableIndices, + alias: this.removableAlias, + }, + } + : undefined, + { + add: { index: this.indexName, alias: this.alias }, + }, + ].filter(Boolean), }, }); diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index a7c12517c7..c53a909f11 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-search-backend-module-pg +## 0.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## 0.3.2 + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-search-backend-node@0.6.0 + +## 0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.2-next.2 + - @backstage/plugin-search-backend-node@0.6.0-next.1 + ## 0.3.2-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index e0dc2a2ad2..b86aa62d2f 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.3.2-next.0", + "version": "0.3.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,15 +23,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/plugin-search-backend-node": "^0.5.3-next.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/plugin-search-backend-node": "^0.6.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", "lodash": "^4.17.21", "knex": "^1.0.2" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1" + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index bf777380b0..23fb80dc3a 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,192 @@ # @backstage/plugin-search-backend-node +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.1-next.0 + +## 0.6.0 + +### Minor Changes + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +### Patch Changes + +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/backend-tasks@0.3.0 + - @backstage/plugin-search-common@0.3.3 + +## 0.6.0-next.1 + +### Minor Changes + +- 0a63e99a26: **BREAKING**: `IndexBuilder.addCollator()` now requires a `schedule` parameter (replacing `defaultRefreshIntervalSeconds`) which is expected to be a `TaskRunner` that is configured with the desired search indexing schedule for the given collator. + + `Scheduler.addToSchedule()` now takes a new parameter object (`ScheduleTaskParameters`) with two new options `id` and `scheduledRunner` in addition to the migrated `task` argument. + + NOTE: The search backend plugin now creates a dedicated database for coordinating indexing tasks. + + To make this change to an existing app, make the following changes to `packages/backend/src/plugins/search.ts`: + + ```diff + +import { Duration } from 'luxon'; + + /* ... */ + + + const schedule = env.scheduler.createScheduledTaskRunner({ + + frequency: Duration.fromObject({ minutes: 10 }), + + timeout: Duration.fromObject({ minutes: 15 }), + + initialDelay: Duration.fromObject({ seconds: 3 }), + + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + const { scheduler } = await indexBuilder.build(); + - setTimeout(() => scheduler.start(), 3000); + + scheduler.start(); + /* ... */ + ``` + + NOTE: For scenarios where the `lunr` search engine is used in a multi-node configuration, a non-distributed `TaskRunner` like the following should be implemented to ensure consistency across nodes (alternatively, you can configure + the search plugin to use a non-distributed DB such as [SQLite](https://backstage.io/docs/tutorials/configuring-plugin-databases#postgresql-and-sqlite-3)): + + ```diff + +import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; + + /* ... */ + + + const schedule: TaskRunner = { + + run: async (task: TaskInvocationDefinition) => { + + const startRefresh = async () => { + + while (!task.signal?.aborted) { + + try { + + await task.fn(task.signal); + + } catch { + + // ignore intentionally + + } + + + + await new Promise(resolve => setTimeout(resolve, 600 * 1000)); + + } + + }; + + startRefresh(); + + }, + + }; + + indexBuilder.addCollator({ + - defaultRefreshIntervalSeconds: 600, + + schedule, + factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + tokenManager: env.tokenManager, + }), + }); + + /* ... */ + ``` + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 3eea462c20..433edcfbd0 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.5.3-next.0", + "version": "0.6.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,9 +23,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-tasks": "^0.3.0-next.2", + "@backstage/backend-tasks": "^0.3.1-next.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/plugin-search-common": "^0.3.3", "@types/lunr": "^2.3.3", "lodash": "^4.17.21", "lunr": "^2.3.9", @@ -33,8 +33,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/cli": "^0.17.0-next.1" + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 5dfcbecf31..656b969541 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-search-backend +## 0.5.2-next.0 + +### Patch Changes + +- 8cc75993a6: Fixed issue in `PermissionEvaluator` instance check that would cause unexpected "invalid union" errors. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-auth-node@0.2.1-next.0 + - @backstage/plugin-permission-node@0.6.1-next.0 + - @backstage/plugin-search-backend-node@0.6.1-next.0 + +## 0.5.0 + +### Minor Changes + +- 94ccd772d4: **BREAKING**: The `authorization` property is no longer returned on search results when queried. Note: this will only result in a breaking change if you have custom code in your frontend that relies on the `authorization.resourceRef` property on documents. + +### Patch Changes + +- 30f9884359: Check for non-resource permissions when authorizing result-by-result in AuthorizedSearchEngine. +- 3c8cfaaa80: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- 62ee65422c: Use new `IndexableResultSet` type as return type of query method in `SearchEngine` implementation. +- c98d271466: Use updated types from `@backstage/plugin-permission-common` +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + - @backstage/plugin-permission-node@0.6.0 + - @backstage/plugin-auth-node@0.2.0 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-search-backend-node@0.6.0 + +## 0.5.0-next.2 + +### Patch Changes + +- 3c8cfaaa80: Use `PermissionEvaluator` instead of `PermissionAuthorizer`, which is now deprecated. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/plugin-permission-node@0.6.0-next.2 + - @backstage/backend-common@0.13.2-next.2 + - @backstage/plugin-search-backend-node@0.6.0-next.1 + ## 0.5.0-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 40a4099c4b..b0e35aecd2 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.5.0-next.1", + "version": "0.5.2-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,14 +23,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/plugin-auth-node": "^0.2.0-next.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-permission-node": "^0.6.0-next.1", - "@backstage/plugin-search-backend-node": "^0.5.3-next.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/plugin-auth-node": "^0.2.1-next.0", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-permission-node": "^0.6.1-next.0", + "@backstage/plugin-search-backend-node": "^0.6.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/types": "^1.0.0", "@types/express": "^4.17.6", "dataloader": "^2.0.0", @@ -43,7 +43,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 11f0eb22e6..e42a31981c 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -76,7 +76,7 @@ export async function createRouter( }); let permissionEvaluator: PermissionEvaluator; - if ('query' in permissions) { + if ('authorizeConditional' in permissions) { permissionEvaluator = permissions as PermissionEvaluator; } else { logger.warn( diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index 5e9cebb34f..f8caf9314b 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search-common +## 0.3.3 + +### Patch Changes + +- c8b295f2fb: - Introduce `SearchDocument` type. This type contains the subset of `IndexableDocument` properties relevant to the frontend, and is intended to be used for documents returned to the frontend from the search API. + - `SearchResultSet` is now a wrapper for documents of type `SearchDocument`, and is intended to be used in the frontend. This isn't a breaking change, since `IndexableDocument`s are valid `SearchDocument`s, so the old and new types are compatible. + - Introduce `IndexableResultSet` type, which wraps `IndexableDocument` instances in the same way as `SearchResultSet`. +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index c21c2aed4b..db0858f7fc 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.3.3-next.1", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -40,10 +40,10 @@ }, "dependencies": { "@backstage/types": "^1.0.0", - "@backstage/plugin-permission-common": "^0.6.0-next.0" + "@backstage/plugin-permission-common": "^0.6.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "jest": { "roots": [ diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md new file mode 100644 index 0000000000..4c3ddea146 --- /dev/null +++ b/plugins/search-react/CHANGELOG.md @@ -0,0 +1,43 @@ +# @backstage/plugin-search-react + +## 0.1.1-next.0 + +### Patch Changes + +- 11a46863de: Export `useSearchContextCheck` hook to check if the search context is available +- a307a14be0: Removed dependency on `@backstage/core-app-api`. + +## 0.1.0 + +### Minor Changes + +- ab230a433f: New search package to hold things the search plugin itself and other frontend plugins (e.g. techdocs, home) depend on. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 076b091113: api-report clean up - the package now exports following additional types: + + `SearchContextProviderProps` + `SearchContextValue` + `SearchContextProviderForStorybookProps` + `SearchApiProviderForStorybookProps` + +- e1de8526aa: Versioned search context managed through version-bridge +- Updated dependencies + - @backstage/core-app-api@1.0.1 + - @backstage/core-plugin-api@1.0.1 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-search-common@0.3.3 + +## 0.1.0-next.0 + +### Minor Changes + +- ab230a433f: New search package to hold things the search plugin itself and other frontend plugins (e.g. techdocs, home) depend on. + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.0.1-next.1 + - @backstage/core-plugin-api@1.0.1-next.0 diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index f9b34e3f3f..969e4d8563 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -18,32 +18,41 @@ export interface SearchApi { query(query: SearchQuery): Promise; } -// Warning: (ae-forgotten-export) The symbol "QueryResultProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "SearchApiProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function SearchApiProviderForStorybook( - props: PropsWithChildren, + props: SearchApiProviderForStorybookProps, ): JSX.Element; +// @public +export type SearchApiProviderForStorybookProps = ComponentProps< + typeof SearchContextProvider +> & { + mockedResults?: SearchResultSet; +}; + // @public (undocumented) export const searchApiRef: ApiRef; -// @public (undocumented) -export const SearchContextProvider: ({ - initialState, - children, -}: React_2.PropsWithChildren<{ - initialState?: SearchContextState | undefined; -}>) => JSX.Element; +// @public +export const SearchContextProvider: ( + props: SearchContextProviderProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const SearchContextProviderForStorybook: ( - props: ComponentProps & QueryResultProps, + props: SearchContextProviderForStorybookProps, ) => JSX.Element; +// @public +export type SearchContextProviderForStorybookProps = PropsWithChildren<{ + mockedResults?: SearchResultSet; +}>; + +// @public +export type SearchContextProviderProps = PropsWithChildren<{ + initialState?: SearchContextState; +}>; + // @public (undocumented) export type SearchContextState = { term: string; @@ -52,10 +61,20 @@ export type SearchContextState = { pageCursor?: string; }; -// Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts -// // @public (undocumented) +export type SearchContextValue = { + result: AsyncState; + setTerm: React_2.Dispatch>; + setTypes: React_2.Dispatch>; + setFilters: React_2.Dispatch>; + setPageCursor: React_2.Dispatch>; + fetchNextPage?: React_2.DispatchWithoutAction; + fetchPreviousPage?: React_2.DispatchWithoutAction; +} & SearchContextState; + +// @public export const useSearch: () => SearchContextValue; -// (No @packageDocumentation comment for this package) +// @public +export const useSearchContextCheck: () => boolean; ``` diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 151660943e..cc883fbda9 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "0.0.0", + "version": "0.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/plugin-search-common": "^0.3.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/core-app-api": "^1.0.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/version-bridge": "^1.0.1", "react-use": "^17.3.2", "@backstage/types": "^1.0.0" }, @@ -42,9 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/core-app-api": "^1.0.1", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/jest-dom": "^5.10.1" }, "files": [ diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index b19953f6a5..0272176150 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -18,7 +18,11 @@ import { useApi } from '@backstage/core-plugin-api'; import { render, screen, waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import React from 'react'; -import { SearchContextProvider, useSearch } from './SearchContext'; +import { + SearchContextProvider, + useSearch, + useSearchContextCheck, +} from './SearchContext'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -71,6 +75,26 @@ describe('SearchContext', () => { ); }); + it('Checks whether context is set', async () => { + const hook = renderHook(() => useSearchContextCheck()); + + expect(hook.result.current).toEqual(false); + + const { result, waitForNextUpdate } = renderHook( + () => useSearchContextCheck(), + { + wrapper, + initialProps: { + initialState, + }, + }, + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual(true); + }); + it('Uses initial state values', async () => { const { result, waitForNextUpdate } = renderHook(() => useSearch(), { wrapper, diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index d6245d5ccf..8a0f962121 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -17,8 +17,11 @@ import { JsonObject } from '@backstage/types'; import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; import { SearchResultSet } from '@backstage/plugin-search-common'; +import { + createVersionedContext, + createVersionedValueMap, +} from '@backstage/version-bridge'; import React, { - createContext, PropsWithChildren, useCallback, useContext, @@ -29,7 +32,11 @@ import useAsync, { AsyncState } from 'react-use/lib/useAsync'; import usePrevious from 'react-use/lib/usePrevious'; import { searchApiRef } from '../api'; -type SearchContextValue = { +/** + * + * @public + */ +export type SearchContextValue = { result: AsyncState; setTerm: React.Dispatch>; setTypes: React.Dispatch>; @@ -50,12 +57,37 @@ export type SearchContextState = { pageCursor?: string; }; +const SearchContext = createVersionedContext<{ + 1: SearchContextValue; +}>('search-context'); + /** * @public + * + * React hook which provides the search context */ -export const SearchContext = createContext( - undefined, -); +export const useSearch = () => { + const context = useContext(SearchContext); + if (!context) { + throw new Error('useSearch must be used within a SearchContextProvider'); + } + + const value = context.atVersion(1); + if (!value) { + throw new Error('No SearchContext v1 found'); + } + return value; +}; + +/** + * @public + * + * React hook which checks for an existing search context + */ +export const useSearchContextCheck = () => { + const context = useContext(SearchContext); + return context !== undefined; +}; /** * The initial state of `SearchContextProvider`. @@ -69,12 +101,21 @@ const searchInitialState: SearchContextState = { }; /** + * Props for {@link SearchContextProvider} + * * @public */ -export const SearchContextProvider = ({ - initialState = searchInitialState, - children, -}: PropsWithChildren<{ initialState?: SearchContextState }>) => { +export type SearchContextProviderProps = PropsWithChildren<{ + initialState?: SearchContextState; +}>; + +/** + * @public + * + * Search context provider which gives you access to shared state between search components + */ +export const SearchContextProvider = (props: SearchContextProviderProps) => { + const { initialState = searchInitialState, children } = props; const searchApi = useApi(searchApiRef); const [pageCursor, setPageCursor] = useState( initialState.pageCursor, @@ -128,20 +169,11 @@ export const SearchContextProvider = ({ fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined, }; + const versionedValue = createVersionedValueMap({ 1: value }); + return ( - + ); }; - -/** - * @public - */ -export const useSearch = () => { - const context = useContext(SearchContext); - if (context === undefined) { - throw new Error('useSearch must be used within a SearchContextProvider'); - } - return context; -}; diff --git a/plugins/search-react/src/context/SearchContextForStorybook.stories.tsx b/plugins/search-react/src/context/SearchContextForStorybook.stories.tsx index 51297c965f..603a89035f 100644 --- a/plugins/search-react/src/context/SearchContextForStorybook.stories.tsx +++ b/plugins/search-react/src/context/SearchContextForStorybook.stories.tsx @@ -18,35 +18,54 @@ import { SearchResultSet } from '@backstage/plugin-search-common'; import { TestApiRegistry } from '@backstage/test-utils'; import React, { ComponentProps, PropsWithChildren } from 'react'; import { searchApiRef } from '../api'; -import { SearchContextProvider as RealSearchContextProvider } from './SearchContext'; +import { SearchContextProvider } from './SearchContext'; -type QueryResultProps = { +/** + * Props for {@link SearchApiProviderForStorybook} + * @public + */ +export type SearchApiProviderForStorybookProps = ComponentProps< + typeof SearchContextProvider +> & { mockedResults?: SearchResultSet; }; +/** + * Props for {@link SearchContextProviderForStorybook} + * @public + */ +export type SearchContextProviderForStorybookProps = PropsWithChildren<{ + mockedResults?: SearchResultSet; +}>; + +/** + * Utility api provider only for use in Storybook stories. + * + * @public + */ +export function SearchApiProviderForStorybook( + props: SearchApiProviderForStorybookProps, +) { + const { mockedResults, children } = props; + const query: any = () => Promise.resolve(mockedResults || {}); + const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); + return ; +} + /** * Utility context provider only for use in Storybook stories. You should use * the real `` exported by `@backstage/plugin-search-react` in * your app instead of this! In some cases (like the search page) it may * already be provided on your behalf. + * + * @public */ -export const SearchContextProvider = ( - props: ComponentProps & QueryResultProps, +export const SearchContextProviderForStorybook = ( + props: SearchContextProviderForStorybookProps, ) => { return ( - - - + + + ); }; - -/** - * Utility api provider only for use in Storybook stories. - * - */ -export function SearchApiProvider(props: PropsWithChildren) { - const { mockedResults, children } = props; - const query: any = () => Promise.resolve(mockedResults || {}); - const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); - return ; -} diff --git a/plugins/search-react/src/context/index.tsx b/plugins/search-react/src/context/index.tsx index f2ea486e9d..76e1142bea 100644 --- a/plugins/search-react/src/context/index.tsx +++ b/plugins/search-react/src/context/index.tsx @@ -16,11 +16,19 @@ export { SearchContextProvider, - SearchContext, useSearch, + useSearchContextCheck, +} from './SearchContext'; +export type { + SearchContextProviderProps, + SearchContextState, + SearchContextValue, } from './SearchContext'; -export type { SearchContextState } from './SearchContext'; export { - SearchContextProvider as SearchContextProviderForStorybook, - SearchApiProvider as SearchApiProviderForStorybook, + SearchContextProviderForStorybook, + SearchApiProviderForStorybook, +} from './SearchContextForStorybook.stories'; +export type { + SearchContextProviderForStorybookProps, + SearchApiProviderForStorybookProps, } from './SearchContextForStorybook.stories'; diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 934cf2e9f1..424c804472 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -14,12 +14,25 @@ * limitations under the License. */ +/** + * Search Plugin frontend library + * + * @packageDocumentation + */ + export { searchApiRef } from './api'; export type { SearchApi } from './api'; export { SearchContextProvider, useSearch, + useSearchContextCheck, SearchContextProviderForStorybook, SearchApiProviderForStorybook, } from './context'; -export type { SearchContextState } from './context'; +export type { + SearchContextProviderProps, + SearchContextState, + SearchContextValue, + SearchContextProviderForStorybookProps, + SearchApiProviderForStorybookProps, +} from './context'; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 7f7735f8b2..bb68809574 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-search +## 0.8.1-next.0 + +### Patch Changes + +- 11a46863de: Fix issue with `HomePageSearchBar` requiring `SearchContext` +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + - @backstage/plugin-search-react@0.1.1-next.0 + +## 0.8.0 + +### Minor Changes + +- 520e21aaea: The following exports has now been fully deleted from this package and can be import from `@backstage/plugin-search-react` instead. + + `SearchApi` interface. + `searchApiRef` + `SearchContextProvider` + `useSearch` + + `SearchContext` has now been fully deleted from this package and is no longer exported publicly. Use `SearchContextProvider` when access to the context is needed. + +### Patch Changes + +- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 5c062f275e: Support customizing the content of the `SidebarSearchModal` +- 38e01f2f70: Switch to `SearchDocument` type in `DefaultResultListItem` props +- 230ad0826f: Bump to using `@types/node` v16 +- ab230a433f: The following exports has been moved to `@backstage/plugin-search-react` and will be removed in the next release. import from `@backstage/plugin-search-react` instead. + + - `SearchApi` interface. + - `searchApiRef` + - `SearchContext` + - `SearchContextProvider` + - `useSearch` + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-react@0.1.0 + - @backstage/plugin-search-common@0.3.3 + +## 0.7.5-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 5c062f275e: Support customizing the content of the `SidebarSearchModal` +- 230ad0826f: Bump to using `@types/node` v16 +- ab230a433f: The following exports has been moved to `@backstage/plugin-search-react` and will be removed in the next release. import from `@backstage/plugin-search-react` instead. + + - `SearchApi` interface. + - `searchApiRef` + - `SearchContext` + - `SearchContextProvider` + - `useSearch` + +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + - @backstage/plugin-search-react@0.1.0-next.0 + ## 0.7.5-next.0 ### Patch Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index d85158c925..31d357f10c 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -5,20 +5,14 @@ ```ts /// -import { ApiRef } from '@backstage/core-plugin-api'; -import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { InputBaseProps } from '@material-ui/core'; -import { JsonObject } from '@backstage/types'; -import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchDocument } from '@backstage/plugin-search-common'; -import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; -import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -81,19 +75,6 @@ export type HomePageSearchBarProps = Partial< // @public (undocumented) export const Router: () => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export interface SearchApi { - // (undocumented) - query(query: SearchQuery): Promise; -} - -// Warning: (ae-missing-release-tag) "searchApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const searchApiRef: ApiRef; - // @public (undocumented) export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; @@ -138,24 +119,6 @@ export const SearchBarNext: ({ // @public export type SearchBarProps = Partial; -// Warning: (ae-missing-release-tag) "SearchContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const SearchContextProvider: ({ - initialState, - children, -}: React_2.PropsWithChildren<{ - initialState?: SearchContextState | undefined; -}>) => JSX.Element; - -// @public -export type SearchContextState = { - term: string; - types: string[]; - filters: JsonObject; - pageCursor?: string; -}; - // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -211,12 +174,19 @@ export const SearchModal: ({ open, hidden, toggleModal, + children, }: SearchModalProps) => JSX.Element; +// @public (undocumented) +export interface SearchModalChildrenProps { + toggleModal: () => void; +} + // Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface SearchModalProps { + children?: (props: SearchModalChildrenProps) => JSX.Element; hidden?: boolean; open?: boolean; toggleModal: () => void; @@ -313,6 +283,7 @@ export const SidebarSearchModal: ( // @public (undocumented) export type SidebarSearchModalProps = { icon?: IconComponent; + children?: (props: SearchModalChildrenProps) => JSX.Element; }; // Warning: (ae-missing-release-tag) "SidebarSearchProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -322,13 +293,6 @@ export type SidebarSearchProps = { icon?: IconComponent; }; -// Warning: (tsdoc-at-sign-in-word) The "@" character looks like part of a TSDoc tag; use a backslash to escape it -// Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "useSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const useSearch: () => SearchContextValue; - // @public export function useSearchModal(initialState?: boolean): { state: { diff --git a/plugins/search/package.json b/plugins/search/package.json index 4ac891775e..e3ccd706c1 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.7.5-next.0", + "version": "0.8.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,14 +33,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", - "@backstage/plugin-search-react": "^0.0.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/plugin-search-react": "^0.1.1-next.0", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", @@ -57,13 +57,13 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@testing-library/react-hooks": "^7.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", "@types/node": "^16.11.26", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 096b11bb5e..208ad98b0a 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -14,30 +14,13 @@ * limitations under the License. */ -import { - createApiRef, - DiscoveryApi, - IdentityApi, -} from '@backstage/core-plugin-api'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; +import { SearchApi } from '@backstage/plugin-search-react'; import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import qs from 'qs'; -/** - * @deprecated import from `@backstage/plugin-search-react` instead - */ -export const searchApiRef = createApiRef({ - id: 'plugin.search.queryservice', -}); - -/** - * @deprecated import from `@backstage/plugin-search-react` instead - */ -export interface SearchApi { - query(query: SearchQuery): Promise; -} - export class SearchClient implements SearchApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx index 1a6c5fbf7b..4c20ddec39 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.stories.tsx @@ -15,7 +15,7 @@ */ import { rootRouteRef, HomePageSearchBar } from '../../plugin'; -import { searchApiRef } from '../../apis'; +import { searchApiRef } from '@backstage/plugin-search-react'; import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { Grid, makeStyles } from '@material-ui/core'; import React, { ComponentType } from 'react'; diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index bccf38ab1d..bc4b409d57 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -16,7 +16,6 @@ import React, { useCallback, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; - import { SearchBarBase, SearchBarBaseProps } from '../SearchBar'; import { useNavigateToQuery } from '../util'; diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index 73afb44f12..c48c3d3ed2 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -17,13 +17,15 @@ import React from 'react'; import { screen, render, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { SearchContextProvider } from '../SearchContext'; - -import { SearchBar } from './SearchBar'; import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { searchApiRef } from '../../apis'; import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; +import { + SearchContextProvider, + searchApiRef, +} from '@backstage/plugin-search-react'; + +import { SearchBar } from './SearchBar'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index a203d27c93..b5d55fed8e 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -20,7 +20,6 @@ import React, { useState, useEffect, useCallback, - useContext, } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; @@ -34,10 +33,10 @@ import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; import { - SearchContext, SearchContextProvider, useSearch, -} from '../SearchContext'; + useSearchContextCheck, +} from '@backstage/plugin-search-react'; import { TrackSearch } from '../SearchTracker'; /** @@ -53,11 +52,6 @@ export type SearchBarBaseProps = Omit & { onChange: (value: string) => void; }; -const useSearchContextCheck = () => { - const context = useContext(SearchContext); - return context !== undefined; -}; - /** * All search boxes exported by the search plugin are based on the , * and this one is based on the component from Material UI. diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx deleted file mode 100644 index c87295ab99..0000000000 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { JsonObject } from '@backstage/types'; -import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchResultSet } from '@backstage/plugin-search-common'; -import React, { - createContext, - PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import useAsync, { AsyncState } from 'react-use/lib/useAsync'; -import usePrevious from 'react-use/lib/usePrevious'; -import { searchApiRef } from '../../apis'; - -type SearchContextValue = { - result: AsyncState; - setTerm: React.Dispatch>; - setTypes: React.Dispatch>; - setFilters: React.Dispatch>; - setPageCursor: React.Dispatch>; - fetchNextPage?: React.DispatchWithoutAction; - fetchPreviousPage?: React.DispatchWithoutAction; -} & SearchContextState; - -/** - * The initial state of `SearchContextProvider`. - * - * @public - */ -export type SearchContextState = { - term: string; - types: string[]; - filters: JsonObject; - pageCursor?: string; -}; - -/** - * @deprecated import from `@backstage/plugin-search-react` instead - */ -export const SearchContext = createContext( - undefined, -); - -const searchInitialState: SearchContextState = { - term: '', - pageCursor: undefined, - filters: {}, - types: [], -}; - -/** - * @deprecated import from `@backstage/plugin-search-react` instead - */ -export const SearchContextProvider = ({ - initialState = searchInitialState, - children, -}: PropsWithChildren<{ initialState?: SearchContextState }>) => { - const searchApi = useApi(searchApiRef); - const [pageCursor, setPageCursor] = useState( - initialState.pageCursor, - ); - const [filters, setFilters] = useState(initialState.filters); - const [term, setTerm] = useState(initialState.term); - const [types, setTypes] = useState(initialState.types); - - const prevTerm = usePrevious(term); - - const result = useAsync( - () => - searchApi.query({ - term, - filters, - pageCursor, - types, - }), - [term, filters, types, pageCursor], - ); - - const hasNextPage = - !result.loading && !result.error && result.value?.nextPageCursor; - const hasPreviousPage = - !result.loading && !result.error && result.value?.previousPageCursor; - const fetchNextPage = useCallback(() => { - setPageCursor(result.value?.nextPageCursor); - }, [result.value?.nextPageCursor]); - const fetchPreviousPage = useCallback(() => { - setPageCursor(result.value?.previousPageCursor); - }, [result.value?.previousPageCursor]); - - useEffect(() => { - // Any time a term is reset, we want to start from page 0. - if (term && prevTerm && term !== prevTerm) { - setPageCursor(undefined); - } - }, [term, prevTerm, initialState.pageCursor]); - - const value: SearchContextValue = { - result, - filters, - setFilters, - term, - setTerm, - types, - setTypes, - pageCursor, - setPageCursor, - fetchNextPage: hasNextPage ? fetchNextPage : undefined, - fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined, - }; - - return ( - - - - ); -}; - -/** - * @deprecated import from "@backstage/plugin-search-react" instead - */ -export const useSearch = () => { - const context = useContext(SearchContext); - if (context === undefined) { - throw new Error('useSearch must be used within a SearchContextProvider'); - } - return context; -}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx index ae0eeb30be..15ad348bd4 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -18,8 +18,11 @@ import { TestApiProvider } from '@backstage/test-utils'; import { screen, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { searchApiRef } from '../../apis'; -import { SearchContextProvider, useSearch } from '../SearchContext'; +import { + SearchContextProvider, + useSearch, + searchApiRef, +} from '@backstage/plugin-search-react'; import { SearchFilter } from './SearchFilter'; const SearchContextFilterSpy = ({ name }: { name: string }) => { diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index 9a27f63565..03f4f28943 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -21,7 +21,7 @@ import { AutocompleteGetTagProps, AutocompleteRenderInputParams, } from '@material-ui/lab'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; import { SearchFilterComponentProps } from './SearchFilter'; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.test.tsx index ba7abe7023..2d652df483 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.test.tsx @@ -19,7 +19,7 @@ import { screen, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SearchFilter } from './SearchFilter'; -import { SearchContextProvider } from '../SearchContext'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; import { useApi } from '@backstage/core-plugin-api'; jest.mock('@backstage/core-plugin-api', () => ({ diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index 1308333100..bfbab8531c 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -30,7 +30,7 @@ import { AutocompleteFilter, SearchAutocompleteFilterProps, } from './SearchFilter.Autocomplete'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; const useStyles = makeStyles({ diff --git a/plugins/search/src/components/SearchFilter/hooks.test.tsx b/plugins/search/src/components/SearchFilter/hooks.test.tsx index d8fbe698ce..14346a29eb 100644 --- a/plugins/search/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search/src/components/SearchFilter/hooks.test.tsx @@ -17,9 +17,12 @@ import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; -import { SearchContextProvider, useSearch } from '../SearchContext'; +import { + SearchContextProvider, + useSearch, + searchApiRef, +} from '@backstage/plugin-search-react'; import { useDefaultFilterValue, useAsyncFilterValues } from './hooks'; -import { searchApiRef } from '../../apis'; jest.useFakeTimers(); diff --git a/plugins/search/src/components/SearchFilter/hooks.ts b/plugins/search/src/components/SearchFilter/hooks.ts index 217e65d1c0..da30466522 100644 --- a/plugins/search/src/components/SearchFilter/hooks.ts +++ b/plugins/search/src/components/SearchFilter/hooks.ts @@ -17,7 +17,7 @@ import { useEffect, useRef } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; /** * Utility hook for either asynchronously loading filter values from a given diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index a1d9517674..e2e3db694b 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -15,11 +15,25 @@ */ import { wrapInTestApp } from '@backstage/test-utils'; -import { Button } from '@material-ui/core'; +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + Grid, + List, + Paper, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; import React, { ComponentType } from 'react'; import { rootRouteRef } from '../../plugin'; +import { DefaultResultListItem } from '../DefaultResultListItem'; +import { SearchBar } from '../SearchBar'; import { SearchApiProviderForStorybook } from '@backstage/plugin-search-react'; import { SearchModal } from './SearchModal'; +import { SearchResult } from '../SearchResult'; +import { SearchResultPager } from '../SearchResultPager'; +import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; const mockResults = { @@ -77,3 +91,87 @@ export const Default = () => { ); }; + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + input: { + flex: 1, + }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, +})); + +export const CustomModal = () => { + const classes = useStyles(); + const { state, toggleModal } = useSearchModal(); + + return ( + <> + + + {() => ( + <> + + + + + + + + + + + + + {({ results }) => ( + + {results.map(({ document }) => ( +
+ +
+ ))} +
+ )} +
+
+
+
+ + + + + + + + + )} +
+ + ); +}; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 9b3f04ad46..842f5fb401 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -21,7 +21,7 @@ import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { rootRouteRef } from '../../plugin'; -import { searchApiRef } from '../../apis'; +import { searchApiRef } from '@backstage/plugin-search-react'; import { SearchModal } from './SearchModal'; @@ -55,6 +55,23 @@ describe('SearchModal', () => { expect(query).toHaveBeenCalledTimes(1); }); + it('Should render a custom Modal correctly', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByText('Custom Search Modal')).toBeInTheDocument(); + }); + it('Calls toggleModal handler', async () => { await renderInTestApp( diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 2a71e53ea8..55fd0cb977 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -31,12 +31,25 @@ import { makeStyles } from '@material-ui/core/styles'; import { SearchBar } from '../SearchBar'; import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResult } from '../SearchResult'; -import { SearchContextProvider, useSearch } from '../SearchContext'; +import { + SearchContextProvider, + useSearch, +} from '@backstage/plugin-search-react'; import { SearchResultPager } from '../SearchResultPager'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; +/** + * @public + **/ +export interface SearchModalChildrenProps { + /** + * A function that should be invoked when navigating away from the modal. + */ + toggleModal: () => void; +} + export interface SearchModalProps { /** * If true, it renders the modal. @@ -54,6 +67,11 @@ export interface SearchModalProps { * should be closed. */ toggleModal: () => void; + /** + * A function that returns custom content to render in the search modal in + * place of the default. + */ + children?: (props: SearchModalChildrenProps) => JSX.Element; } const useStyles = makeStyles(theme => ({ @@ -152,6 +170,7 @@ export const SearchModal = ({ open = true, hidden, toggleModal, + children, }: SearchModalProps) => { const classes = useStyles(); @@ -169,7 +188,9 @@ export const SearchModal = ({ > {open && ( - + {(children && children({ toggleModal })) ?? ( + + )} )} diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx index 646dff0a62..51e5d94c07 100644 --- a/plugins/search/src/components/SearchModal/index.tsx +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ export { SearchModal } from './SearchModal'; -export type { SearchModalProps } from './SearchModal'; +export type { SearchModalChildrenProps, SearchModalProps } from './SearchModal'; export { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx index 9486196584..59e6e99aaf 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx @@ -17,7 +17,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { useLocation, useOutlet } from 'react-router'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { SearchPage } from './SearchPage'; jest.mock('react-router', () => ({ @@ -33,8 +33,8 @@ const setTypesMock = jest.fn(); const setFiltersMock = jest.fn(); const setPageCursorMock = jest.fn(); -jest.mock('../SearchContext', () => ({ - ...jest.requireActual('../SearchContext'), +jest.mock('@backstage/plugin-search-react', () => ({ + ...jest.requireActual('@backstage/plugin-search-react'), SearchContextProvider: jest .fn() .mockImplementation(({ children }) => children), @@ -51,7 +51,7 @@ jest.mock('../SearchContext', () => ({ })); jest.mock('../LegacySearchPage', () => ({ - ...jest.requireActual('../SearchContext'), + ...jest.requireActual('@backstage/plugin-search-react'), LegacySearchPage: jest.fn().mockReturnValue('LegacySearchPageMock'), })); diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index e4e1e49d57..29a22a6f5c 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -18,7 +18,10 @@ import React, { useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import qs from 'qs'; import { useLocation, useOutlet } from 'react-router'; -import { SearchContextProvider, useSearch } from '../SearchContext'; +import { + SearchContextProvider, + useSearch, +} from '@backstage/plugin-search-react'; import { JsonObject } from '@backstage/types'; import { LegacySearchPage } from '../LegacySearchPage'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.test.tsx b/plugins/search/src/components/SearchResult/SearchResult.test.tsx index 43a9cdb022..d93eacaef0 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.test.tsx @@ -17,11 +17,11 @@ import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { SearchResult } from './SearchResult'; -jest.mock('../SearchContext', () => ({ - ...jest.requireActual('../SearchContext'), +jest.mock('@backstage/plugin-search-react', () => ({ + ...jest.requireActual('@backstage/plugin-search-react'), useSearch: jest.fn().mockReturnValue({ result: {}, }), diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index d6ca9fd968..a62dddb924 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -21,7 +21,7 @@ import { } from '@backstage/core-components'; import { SearchResult } from '@backstage/plugin-search-common'; import React from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; type Props = { children: (results: { results: SearchResult[] }) => JSX.Element; diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.test.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.test.tsx index f24a73d63e..b1cad8d748 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.test.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.test.tsx @@ -18,11 +18,11 @@ import { renderInTestApp } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { SearchResultPager } from './SearchResultPager'; -jest.mock('../SearchContext', () => ({ - ...jest.requireActual('../SearchContext'), +jest.mock('@backstage/plugin-search-react', () => ({ + ...jest.requireActual('@backstage/plugin-search-react'), useSearch: jest.fn().mockReturnValue({ result: {}, }), diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index 12f07530ef..6f4c8628a6 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -18,7 +18,7 @@ import { Button, makeStyles } from '@material-ui/core'; import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import React from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; const useStyles = makeStyles(theme => ({ root: { diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search/src/components/SearchTracker/SearchTracker.tsx index b9da9b5560..35cfa1ec38 100644 --- a/plugins/search/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search/src/components/SearchTracker/SearchTracker.tsx @@ -16,7 +16,7 @@ import React, { useEffect } from 'react'; import { useAnalytics } from '@backstage/core-plugin-api'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; /** * Capture search event on term change. diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index a62d061663..da1cae46b9 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -15,29 +15,30 @@ */ import React from 'react'; -import { ApiProvider } from '@backstage/core-app-api'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; - -import { searchApiRef } from '../../apis'; -import { SearchContext, SearchContextProvider } from '../SearchContext'; +import { + searchApiRef, + SearchContextProvider, +} from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; +const setTypesMock = jest.fn(); +const setPageCursorMock = jest.fn(); + +jest.mock('@backstage/plugin-search-react', () => ({ + ...jest.requireActual('@backstage/plugin-search-react'), + useSearch: jest.fn().mockReturnValue({ + types: [], + setTypes: (types: any) => setTypesMock(types), + pageCursor: '', + setPageCursor: (pageCursor: any) => setPageCursorMock(pageCursor), + }), +})); + describe('SearchType.Accordion', () => { const query = jest.fn(); - const mockApis = TestApiRegistry.from([searchApiRef, { query }]); - - const contextSpy = { - result: { loading: false, value: { results: [] } }, - term: '', - types: [], - filters: {}, - setTerm: jest.fn(), - setTypes: jest.fn(), - setFilters: jest.fn(), - setPageCursor: jest.fn(), - }; const expectedLabel = 'Expected Label'; const expectedType = { @@ -50,17 +51,19 @@ describe('SearchType.Accordion', () => { query.mockResolvedValue({ results: [] }); }); - afterEach(() => { - jest.resetAllMocks(); - }); + const Wrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }; it('should render as expected', async () => { const { getByText } = render( - - - - - , + + + , ); // The given label should be rendered. @@ -80,49 +83,49 @@ describe('SearchType.Accordion', () => { it('should set entire types array when a type is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText(expectedType.name)); - expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]); + expect(setTypesMock).toHaveBeenCalledWith([expectedType.value]); }); it('should reset types array when all is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText('All')); - expect(contextSpy.setTypes).toHaveBeenCalledWith([]); + expect(setTypesMock).toHaveBeenCalledWith([]); }); it('should reset page cursor when a new type is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText(expectedType.name)); - expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined); + expect(setPageCursorMock).toHaveBeenCalledWith(undefined); }); it('should collapse when a new type is selected', async () => { const { getByText, queryByText } = render( - + - , + , ); await user.click(getByText(expectedType.name)); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 57e6609bab..49f5cf74ca 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -15,7 +15,7 @@ */ import React, { cloneElement, Fragment, useEffect, useState } from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { Accordion, AccordionSummary, diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index 2886b63ae7..16df3a8f65 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -15,50 +15,49 @@ */ import React from 'react'; -import { ApiProvider } from '@backstage/core-app-api'; -import { TestApiRegistry } from '@backstage/test-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; - -import { searchApiRef } from '../../apis'; -import { SearchContext, SearchContextProvider } from '../SearchContext'; +import { + SearchContextProvider, + searchApiRef, +} from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; -describe('SearchType.Tabs', () => { - const query = jest.fn(); - const mockApis = TestApiRegistry.from([searchApiRef, { query }]); +const setTypesMock = jest.fn(); +const setPageCursorMock = jest.fn(); - const contextSpy = { - result: { loading: false, value: { results: [] } }, - term: '', +jest.mock('@backstage/plugin-search-react', () => ({ + ...jest.requireActual('@backstage/plugin-search-react'), + useSearch: jest.fn().mockReturnValue({ types: [], - filters: {}, - setTerm: jest.fn(), - setTypes: jest.fn(), - setFilters: jest.fn(), - setPageCursor: jest.fn(), - }; + setTypes: (types: any) => setTypesMock(types), + pageCursor: '', + setPageCursor: (pageCursor: any) => setPageCursorMock(pageCursor), + }), +})); + +describe('SearchType.Tabs', () => { + const query = jest.fn().mockResolvedValue({}); const expectedType = { value: 'expected-type', name: 'Expected Type', }; - beforeEach(() => { - query.mockResolvedValue({ results: [] }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); + const Wrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }; it('should render as expected', async () => { const { getByText } = render( - - - - - , + + + , ); // The default "all" type should be rendered. @@ -72,40 +71,40 @@ describe('SearchType.Tabs', () => { it('should set entire types array when a type is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText(expectedType.name)); - expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]); + expect(setTypesMock).toHaveBeenCalledWith([expectedType.value]); }); it('should reset types array when all is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText('All')); - expect(contextSpy.setTypes).toHaveBeenCalledWith([]); + expect(setTypesMock).toHaveBeenCalledWith([]); }); it('should reset page cursor when a new type is selected', async () => { const { getByText } = render( - + - , + , ); await user.click(getByText(expectedType.name)); - expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined); + expect(setPageCursorMock).toHaveBeenCalledWith(undefined); }); }); diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx index 465afd2de4..fc0380e23a 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect } from 'react'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, Tab, Tabs } from '@material-ui/core'; @@ -77,8 +77,9 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => { value={types.length === 0 ? '' : types[0]} onChange={changeTab} > - {definedTypes.map(type => ( + {definedTypes.map((type, idx) => ( ({ diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index ba20e1db5d..79c3dda2b9 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -30,7 +30,7 @@ import { SearchTypeAccordionProps, } from './SearchType.Accordion'; import { SearchTypeTabs, SearchTypeTabsProps } from './SearchType.Tabs'; -import { useSearch } from '../SearchContext'; +import { useSearch } from '@backstage/plugin-search-react'; const useStyles = makeStyles(theme => ({ label: { diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index bbf4eef4e4..4310c03b15 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -17,10 +17,15 @@ import React from 'react'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarItem } from '@backstage/core-components'; import { IconComponent } from '@backstage/core-plugin-api'; -import { SearchModal, useSearchModal } from '../SearchModal'; +import { + SearchModal, + SearchModalChildrenProps, + useSearchModal, +} from '../SearchModal'; export type SidebarSearchModalProps = { icon?: IconComponent; + children?: (props: SearchModalChildrenProps) => JSX.Element; }; export const SidebarSearchModal = (props: SidebarSearchModalProps) => { @@ -35,7 +40,11 @@ export const SidebarSearchModal = (props: SidebarSearchModalProps) => { text="Search" onClick={toggleModal} /> - + ); }; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index cef0355fb4..dccb2ce835 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -20,9 +20,6 @@ * @packageDocumentation */ -export { searchApiRef } from './apis'; -export type { SearchApi } from './apis'; - export { Filters, FiltersButton } from './components/Filters'; export type { FiltersState } from './components/Filters'; export type { HomePageSearchBarProps } from './components/HomePageComponent'; @@ -31,8 +28,6 @@ export type { SearchBarBaseProps, SearchBarProps, } from './components/SearchBar'; -export { SearchContextProvider, useSearch } from './components/SearchContext'; -export type { SearchContextState } from './components/SearchContext'; export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; export type { SearchAutocompleteFilterProps, @@ -40,7 +35,10 @@ export type { SearchFilterWrapperProps, } from './components/SearchFilter'; export { SearchModal, useSearchModal } from './components/SearchModal'; -export type { SearchModalProps } from './components/SearchModal'; +export type { + SearchModalChildrenProps, + SearchModalProps, +} from './components/SearchModal'; export { SearchPage as Router } from './components/SearchPage'; export { SearchResultPager } from './components/SearchResultPager'; export { SearchType } from './components/SearchType'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 9dda2a84bb..96e7bc1eed 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { SearchClient, searchApiRef } from './apis'; +import { SearchClient } from './apis'; +import { searchApiRef } from '@backstage/plugin-search-react'; import { createApiFactory, createPlugin, diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 26c3c47fc4..6ccd6652bc 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-sentry +## 0.3.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.42 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 68bb66102d: Exposed options for the material table +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.42-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.42-next.1 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index f150fb927c..c6ef1e974f 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.42-next.1", + "version": "0.3.43-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.1", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.2", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index aa0afe0375..80f5007e59 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-shortcuts +## 0.2.5 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.2.5-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.2.5-next.0 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 0d2c41e7de..adf2c26686 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.2.5-next.0", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 91bab9f82e..53d9d93d16 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-sonarqube +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.4 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 2b227933da..86684b08e3 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.3.4-next.0", + "version": "0.3.5-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index cf333b6d41..9d64906428 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-splunk-on-call +## 0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.3.28 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.3.28-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.3.28-next.0 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 56acc410b3..b9db1a4f3b 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.28-next.0", + "version": "0.3.29-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/stack-overflow-backend/CHANGELOG.md b/plugins/stack-overflow-backend/CHANGELOG.md index 5ac17f0a79..de81256acb 100644 --- a/plugins/stack-overflow-backend/CHANGELOG.md +++ b/plugins/stack-overflow-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-stack-overflow-backend +## 0.1.0 + +### Minor Changes + +- ac323de4ad: Add stack overflow backend plugin + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@0.3.3 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index ccc2b0b5fe..19bfe5d68d 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", - "version": "0.1.0-next.0", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/config": "^1.0.0", "qs": "^6.9.4", "cross-fetch": "^3.1.5", diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index 2bfa058580..4e1a3e957c 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-stack-overflow +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home@0.4.21-next.0 + +## 0.1.0 + +### Minor Changes + +- ac323de4ad: Add stack overflow plugin + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-home@0.4.20 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + - @backstage/plugin-search-common@0.3.3 + +## 0.1.0-next.1 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/plugin-home@0.4.20-next.3 + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 52f9f4cabf..ba91b34818 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.0-next.0", + "version": "0.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ }, "dependencies": { "@backstage/config": "^1.0.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", - "@backstage/plugin-home": "^0.4.20-next.1", - "@backstage/plugin-search-common": "^0.3.3-next.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-home": "^0.4.21-next.0", + "@backstage/plugin-search-common": "^0.3.3", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/jest": "^26.0.7", diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index f3bc32b796..298303dcf3 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + +## 0.1.15 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- ab008a0988: Removes node-cron from tech-insights to utilize backend-tasks +- Updated dependencies + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/backend-common@0.13.2 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/README.md b/plugins/tech-insights-backend-module-jsonfc/README.md index 70b2ef2c40..f61666d671 100644 --- a/plugins/tech-insights-backend-module-jsonfc/README.md +++ b/plugins/tech-insights-backend-module-jsonfc/README.md @@ -28,6 +28,7 @@ and modify the `techInsights.ts` file to contain a reference to the FactCheckers config: env.config, database: env.database, discovery: env.discovery, + tokenManager: env.tokenManager, factRetrievers: [myFactRetrieverRegistration], + factCheckerFactory: myFactCheckerFactory }); diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 6ffa20747a..b03b8294b7 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.15-next.1", + "version": "0.1.16-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@backstage/plugin-tech-insights-common": "^0.2.4", - "@backstage/plugin-tech-insights-node": "^0.2.9-next.1", + "@backstage/plugin-tech-insights-node": "^0.2.10-next.0", "ajv": "^8.10.0", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -46,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index d52d8e7b9f..1f6828a777 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-tech-insights-backend +## 0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/backend-tasks@0.3.1-next.0 + - @backstage/plugin-tech-insights-node@0.2.10-next.0 + +## 0.3.0 + +### Minor Changes + +- 231fee736b: This backend now uses the `@backstage/backend-tasks` package facilities for scheduling fact retrievers. + + **BREAKING**: The `buildTechInsightsContext` function now takes an additional field in its options argument: `scheduler`. This is an instance of `PluginTaskScheduler`, which can be found in your backend initialization code's `env`. + + ```diff + const builder = buildTechInsightsContext({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + + scheduler: env.scheduler, + factRetrievers: [ /* ... */ ], + }); + ``` + +### Patch Changes + +- 21de525ce9: Updated README.md with better install instructions +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. +- 2fe58c7285: Improved the Tech-Insights documentation: + + - `lifecycle` examples used `ttl` when it should be `timeToLive` + - Added list of included FactRetrievers + - Added full backend example using all included FactRetrievers + - Added boolean scorecard example image showing results of backend example + +- Updated dependencies + - @backstage/backend-tasks@0.3.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-tech-insights-node@0.2.9 + - @backstage/backend-common@0.13.2 + - @backstage/catalog-client@1.0.1 + ## 0.3.0-next.2 ### Patch Changes diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index ee6ef4c0cc..e88d150393 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -35,6 +35,7 @@ export default async function createPlugin( database: env.database, discovery: env.discovery, scheduler: env.scheduler, + tokenManager: env.tokenManager, factRetrievers: [], // Fact retrievers registrations you want tech insights to use }); @@ -104,6 +105,7 @@ const builder = buildTechInsightsContext({ config: env.config, database: env.database, discovery: env.discovery, + tokenManager: env.tokenManager, - factRetrievers: [], + factRetrievers: [myFactRetrieverRegistration], }); @@ -119,6 +121,7 @@ const builder = buildTechInsightsContext({ config: env.config, database: env.database, discovery: env.discovery, + tokenManager: env.tokenManager, - factRetrievers: [], + factRetrievers: process.env.MAIN_FACT_RETRIEVER_INSTANCE ? [myFactRetrieverRegistration] : [], }); @@ -158,9 +161,12 @@ const myFactRetriever: FactRetriever = { const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities({ - filter: [{ kind: 'component' }], - }); + const entities = await catalogClient.getEntities( + { + filter: [{ kind: 'component' }], + }, + { token }, + ); /** * snip: Do complex logic to retrieve facts from external system or calculate fact values */ @@ -214,6 +220,7 @@ and modify the `techInsights.ts` file to contain a reference to the FactChecker config: env.config, database: env.database, discovery: env.discovery, + tokenManager: env.tokenManager, factRetrievers: [myFactRetrieverRegistration], + factCheckerFactory: myFactCheckerFactory }); @@ -273,6 +280,7 @@ export default async function createPlugin( config: env.config, database: env.database, discovery: env.discovery, + tokenManager: env.tokenManager, factRetrievers: [ createFactRetrieverRegistration({ cadence: '0 */6 * * *', // Run every 6 hours - https://crontab.guru/#0_*/6_*_*_* diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 210aa50af9..9fee3be281 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; import { TechInsightsStore } from '@backstage/plugin-tech-insights-node'; +import { TokenManager } from '@backstage/backend-common'; // @public export const buildTechInsightsContext: < @@ -95,6 +96,8 @@ export interface TechInsightsOptions< logger: Logger; // (undocumented) scheduler: PluginTaskScheduler; + // (undocumented) + tokenManager: TokenManager; } // (No @packageDocumentation comment for this package) diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index d63299c2dd..c3fccea8ad 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.3.0-next.2", + "version": "0.3.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,14 +34,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/backend-tasks": "^0.3.0-next.2", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.1", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/backend-tasks": "^0.3.1-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@backstage/plugin-tech-insights-common": "^0.2.4", - "@backstage/plugin-tech-insights-node": "^0.2.9-next.1", + "@backstage/plugin-tech-insights-node": "^0.2.10-next.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -54,8 +54,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.2", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", "@types/supertest": "^2.0.8", "@types/semver": "^7.3.8", "supertest": "^6.1.3", diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 0617c0aedd..454711171f 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -22,7 +22,11 @@ import { } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistry } from './FactRetrieverRegistry'; import { FactRetrieverEngine } from './FactRetrieverEngine'; -import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; +import { + DatabaseManager, + getVoidLogger, + ServerTokenManager, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { TaskScheduler } from '@backstage/backend-tasks'; @@ -129,6 +133,7 @@ describe('FactRetrieverEngine', () => { factRetrieverContext: { logger: getVoidLogger(), config: ConfigReader.fromConfigs([]), + tokenManager: ServerTokenManager.noop(), discovery: { getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts index 9d5c57637d..31400788b5 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts @@ -18,6 +18,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, + ServerTokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; @@ -104,6 +105,7 @@ const handlerContext = { discovery, logger: getVoidLogger(), config: ConfigReader.fromConfigs([]), + tokenManager: ServerTokenManager.noop(), }; const entityFactRetriever = entityMetadataFactRetriever; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts index 26b1e4274b..caa19dbeaf 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts @@ -44,11 +44,19 @@ export const entityMetadataFactRetriever: FactRetriever = { description: 'The entity has tags in metadata', }, }, - handler: async ({ discovery, entityFilter }: FactRetrieverContext) => { + handler: async ({ + discovery, + entityFilter, + tokenManager, + }: FactRetrieverContext) => { + const { token } = await tokenManager.getToken(); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities({ filter: entityFilter }); + const entities = await catalogClient.getEntities( + { filter: entityFilter }, + { token }, + ); return entities.items.map((entity: Entity) => { return { diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts index 555f761ae8..7a77ef91c4 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts @@ -19,6 +19,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, + ServerTokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; @@ -104,6 +105,7 @@ const handlerContext = { discovery, logger: getVoidLogger(), config: ConfigReader.fromConfigs([]), + tokenManager: ServerTokenManager.noop(), }; const entityFactRetriever = entityOwnershipFactRetriever; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts index b9a91e67f7..c1e9b2710e 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts @@ -42,11 +42,19 @@ export const entityOwnershipFactRetriever: FactRetriever = { description: 'The spec.owner field is set and refers to a group', }, }, - handler: async ({ discovery, entityFilter }: FactRetrieverContext) => { + handler: async ({ + discovery, + entityFilter, + tokenManager, + }: FactRetrieverContext) => { + const { token } = await tokenManager.getToken(); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities({ filter: entityFilter }); + const entities = await catalogClient.getEntities( + { filter: entityFilter }, + { token }, + ); return entities.items.map((entity: Entity) => { return { diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts index be9397942d..0c70f6c9f1 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts @@ -19,6 +19,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, + ServerTokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { GetEntitiesResponse } from '@backstage/catalog-client'; @@ -104,6 +105,7 @@ const handlerContext = { discovery, logger: getVoidLogger(), config: ConfigReader.fromConfigs([]), + tokenManager: ServerTokenManager.noop(), }; const entityFactRetriever = techdocsFactRetriever; diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts index 2717f94475..6bc84f4aa9 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts @@ -40,11 +40,19 @@ export const techdocsFactRetriever: FactRetriever = { description: 'The entity has a title in metadata', }, }, - handler: async ({ discovery, entityFilter }: FactRetrieverContext) => { + handler: async ({ + discovery, + entityFilter, + tokenManager, + }: FactRetrieverContext) => { + const { token } = await tokenManager.getToken(); const catalogClient = new CatalogClient({ discoveryApi: discovery, }); - const entities = await catalogClient.getEntities({ filter: entityFilter }); + const entities = await catalogClient.getEntities( + { filter: entityFilter }, + { token }, + ); return entities.items.map((entity: Entity) => { return { diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index df2d471516..8513973598 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -19,6 +19,7 @@ import { DatabaseManager, getVoidLogger, PluginDatabaseManager, + ServerTokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import request from 'supertest'; @@ -74,6 +75,7 @@ describe('Tech Insights router tests', () => { getBaseUrl: (_: string) => Promise.resolve('http://mock.url'), getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'), }, + tokenManager: ServerTokenManager.noop(), }); const router = await createRouter({ diff --git a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts index 4f8f2ed928..e22bfbd253 100644 --- a/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts +++ b/plugins/tech-insights-backend/src/service/techInsightsContextBuilder.ts @@ -21,6 +21,7 @@ import { Config } from '@backstage/config'; import { PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, } from '@backstage/backend-common'; import { FactChecker, @@ -63,6 +64,7 @@ export interface TechInsightsOptions< discovery: PluginEndpointDiscovery; database: PluginDatabaseManager; scheduler: PluginTaskScheduler; + tokenManager: TokenManager; } /** @@ -104,6 +106,7 @@ export const buildTechInsightsContext = async < database, logger, scheduler, + tokenManager, } = options; const factRetrieverRegistry = new FactRetrieverRegistry(factRetrievers); @@ -121,6 +124,7 @@ export const buildTechInsightsContext = async < config, discovery, logger, + tokenManager, }, }); diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index f222a935b9..f2293f403e 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -38,7 +38,7 @@ "@backstage/types": "^1.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index a57c0b018f..5f35343b9c 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-tech-insights-node +## 0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + +## 0.2.9 + +### Patch Changes + +- 231fee736b: Adds an optional timeout to fact retriever registrations to stop a task if it runs too long. +- Updated dependencies + - @backstage/backend-common@0.13.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 7d9f2e00b7..bcccec65b5 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -10,6 +10,7 @@ import { Duration } from 'luxon'; import { DurationLike } from 'luxon'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { TokenManager } from '@backstage/backend-common'; // @public export type CheckValidationResponse = { @@ -58,6 +59,7 @@ export type FactRetrieverContext = { config: Config; discovery: PluginEndpointDiscovery; logger: Logger; + tokenManager: TokenManager; entityFilter?: | Record[] | Record; diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 3c44f06558..a843351fd0 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.2.9-next.1", + "version": "0.2.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", + "@backstage/backend-common": "^0.13.3-next.0", "@backstage/config": "^1.0.0", "@backstage/plugin-tech-insights-common": "^0.2.4", "@types/luxon": "^2.0.5", @@ -41,7 +41,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.1" + "@backstage/cli": "^0.17.1-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index a5df4bae85..ffd3549bd5 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -15,7 +15,10 @@ */ import { DateTime, Duration, DurationLike } from 'luxon'; import { Config } from '@backstage/config'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Logger } from 'winston'; /** @@ -135,6 +138,7 @@ export type FactRetrieverContext = { config: Config; discovery: PluginEndpointDiscovery; logger: Logger; + tokenManager: TokenManager; entityFilter?: | Record[] | Record; diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index f8663f785f..75e836f0dc 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-tech-insights +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.1.0-next.0 + +## 0.2.0 + +### Minor Changes + +- 567b13a84a: Add checksId option to EntityTechInsightsScorecardContent component + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- 2fe58c7285: Improved the Tech-Insights documentation: + + - `lifecycle` examples used `ttl` when it should be `timeToLive` + - Added list of included FactRetrievers + - Added full backend example using all included FactRetrievers + - Added boolean scorecard example image showing results of backend example + +- Updated dependencies + - @backstage/plugin-catalog-react@1.0.1 + - @backstage/catalog-model@1.0.1 + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.2.0-next.1 + +### Minor Changes + +- 567b13a84a: Add checksId option to EntityTechInsightsScorecardContent component + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.0.1-next.3 + ## 0.1.14-next.0 ### Patch Changes diff --git a/plugins/tech-insights/README.md b/plugins/tech-insights/README.md index a305ef5dc7..6c13622e1f 100644 --- a/plugins/tech-insights/README.md +++ b/plugins/tech-insights/README.md @@ -38,10 +38,6 @@ const serviceEntityPage = ( title="Customized title for the scorecard" description="Small description about scorecards" /> - ... @@ -50,8 +46,45 @@ const serviceEntityPage = ( It is not obligatory to pass title and description props to `EntityTechInsightsScorecardContent`. If those are left out, default values from `defaultCheckResultRenderers` in `CheckResultRenderer` will be taken, hence `Boolean scorecard` and `This card represents an overview of default boolean Backstage checks`. +If you like to display multiple cards in a `EntityLayout.Route` use `EntityTechInsightsScorecardCard`. + You can pass an array `checksId` as a prop with the [Fact Retrievers ids](../tech-insights-backend#creating-fact-retrievers) to limit which checks you want to show in this card, If you don't pass, the default value is show all checks. +```tsx + +``` + +If you want to show checks in the overview of an entity use `EntityTechInsightsScorecardCard`. + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityTechInsightsScorecardCard } from '@backstage/plugin-tech-insights'; + +const overviewContent = ( + + {entityWarningContent} + + + + + + + ... + + + + +); +``` + ## Boolean Scorecard Example If you follow the [Backend Example](https://github.com/backstage/backstage/tree/master/plugins/tech-insights-backend#backend-example), once the needed facts have been generated the boolean scorecard will look like this: diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 9574efca28..3e099493b6 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -30,6 +30,17 @@ export type CheckResultRenderer = { component: React_2.ReactElement; }; +// @public (undocumented) +export const EntityTechInsightsScorecardCard: ({ + title, + description, + checksId, +}: { + title?: string | undefined; + description?: string | undefined; + checksId?: string[] | undefined; +}) => JSX.Element; + // @public (undocumented) export const EntityTechInsightsScorecardContent: ({ title, diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 2ec49bf21a..21d80a7890 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.1.14-next.0", + "version": "0.2.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,11 +28,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^1.0.1-next.0", - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/catalog-model": "^1.0.1", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/errors": "^1.0.0", - "@backstage/plugin-catalog-react": "^1.0.1-next.1", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", "@backstage/plugin-tech-insights-common": "^0.2.4", "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx b/plugins/tech-insights/src/components/ScorecardsCard/ScorecardsCard.tsx similarity index 93% rename from plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx rename to plugins/tech-insights/src/components/ScorecardsCard/ScorecardsCard.tsx index 53cf826e78..f6bc4b0cb5 100644 --- a/plugins/tech-insights/src/components/ScorecardsOverview/ScorecardsOverview.tsx +++ b/plugins/tech-insights/src/components/ScorecardsCard/ScorecardsCard.tsx @@ -19,11 +19,11 @@ import { useParams } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { Progress } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { ChecksOverview } from './ChecksOverview'; +import { ScorecardInfo } from '../ScorecardsInfo'; import Alert from '@material-ui/lab/Alert'; import { techInsightsApiRef } from '../../api/TechInsightsApi'; -export const ScorecardsOverview = ({ +export const ScorecardsCard = ({ title, description, checksId, @@ -45,7 +45,7 @@ export const ScorecardsOverview = ({ } return ( - ({ + contentScorecards: { + paddingLeft: 0, + paddingRight: 0, + }, +})); + +export const ScorecardsContent = ({ + title, + description, + checksId, +}: { + title?: string; + description?: string; + checksId?: string[]; +}) => { + const classes = useStyles(); + const api = useApi(techInsightsApiRef); + const { namespace, kind, name } = useParams(); + const { value, loading, error } = useAsync( + async () => await api.runChecks({ namespace, kind, name }, checksId), + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + + + + + ); +}; diff --git a/plugins/search/src/components/SearchContext/index.tsx b/plugins/tech-insights/src/components/ScorecardsContent/index.ts similarity index 80% rename from plugins/search/src/components/SearchContext/index.tsx rename to plugins/tech-insights/src/components/ScorecardsContent/index.ts index 8651e661c1..fcdfb0b694 100644 --- a/plugins/search/src/components/SearchContext/index.tsx +++ b/plugins/tech-insights/src/components/ScorecardsContent/index.ts @@ -14,10 +14,4 @@ * limitations under the License. */ -export { - SearchContextProvider, - SearchContext, - useSearch, -} from './SearchContext'; - -export type { SearchContextState } from './SearchContext'; +export { ScorecardsContent } from './ScorecardContent'; diff --git a/plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx b/plugins/tech-insights/src/components/ScorecardsInfo/ScorecardInfo.tsx similarity index 64% rename from plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx rename to plugins/tech-insights/src/components/ScorecardsInfo/ScorecardInfo.tsx index d153c690eb..2c5abb3596 100644 --- a/plugins/tech-insights/src/components/ScorecardsOverview/ChecksOverview.tsx +++ b/plugins/tech-insights/src/components/ScorecardsInfo/ScorecardInfo.tsx @@ -17,17 +17,13 @@ import React from 'react'; import { makeStyles, Grid, Typography } from '@material-ui/core'; import { useApi } from '@backstage/core-plugin-api'; -import { Content, Page, InfoCard } from '@backstage/core-components'; +import { InfoCard } from '@backstage/core-components'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { techInsightsApiRef } from '../../api/TechInsightsApi'; import { BackstageTheme } from '@backstage/theme'; import { Alert } from '@material-ui/lab'; const useStyles = makeStyles((theme: BackstageTheme) => ({ - contentScorecards: { - paddingLeft: 0, - paddingRight: 0, - }, subheader: { fontWeight: 'bold', paddingLeft: theme.spacing(0.5), @@ -40,12 +36,37 @@ type Checks = { description?: string; }; -export const ChecksOverview = ({ checks, title, description }: Checks) => { +const infoCard = ( + title: Checks['title'], + description: Checks['description'], + className: string, + element: JSX.Element, +) => ( + + + + {description} + + + {element} + + + +); + +export const ScorecardInfo = ({ checks, title, description }: Checks) => { const classes = useStyles(); const api = useApi(techInsightsApiRef); + if (!checks.length) { - return No checks have any data yet.; + return infoCard( + title, + description, + classes.subheader, + No checks have any data yet., + ); } + const checkRenderType = api.getScorecardsDefinition( checks[0].check.type, checks, @@ -54,21 +75,11 @@ export const ChecksOverview = ({ checks, title, description }: Checks) => { ); if (checkRenderType) { - return ( - - - - - - {checkRenderType.description} - - - {checkRenderType.component} - - - - - + return infoCard( + checkRenderType.title, + checkRenderType.description, + classes.subheader, + checkRenderType.component, ); } diff --git a/plugins/tech-insights/src/components/ScorecardsInfo/index.ts b/plugins/tech-insights/src/components/ScorecardsInfo/index.ts new file mode 100644 index 0000000000..21928465e0 --- /dev/null +++ b/plugins/tech-insights/src/components/ScorecardsInfo/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ScorecardInfo } from './ScorecardInfo'; diff --git a/plugins/tech-insights/src/index.ts b/plugins/tech-insights/src/index.ts index b59ac453ce..bc7c2ce26c 100644 --- a/plugins/tech-insights/src/index.ts +++ b/plugins/tech-insights/src/index.ts @@ -16,6 +16,7 @@ export { techInsightsPlugin, EntityTechInsightsScorecardContent, + EntityTechInsightsScorecardCard, } from './plugin'; export { techInsightsApiRef } from './api/TechInsightsApi'; diff --git a/plugins/tech-insights/src/plugin.ts b/plugins/tech-insights/src/plugin.ts index 0c2da63670..961519cd97 100644 --- a/plugins/tech-insights/src/plugin.ts +++ b/plugins/tech-insights/src/plugin.ts @@ -49,7 +49,19 @@ export const EntityTechInsightsScorecardContent = techInsightsPlugin.provide( createRoutableExtension({ name: 'EntityTechInsightsScorecardContent', component: () => - import('./components/ScorecardsOverview').then(m => m.ScorecardsOverview), + import('./components/ScorecardsContent').then(m => m.ScorecardsContent), + mountPoint: rootRouteRef, + }), +); + +/** + * @public + */ +export const EntityTechInsightsScorecardCard = techInsightsPlugin.provide( + createRoutableExtension({ + name: 'EntityTechInsightsScorecardCard', + component: () => + import('./components/ScorecardsCard').then(m => m.ScorecardsCard), mountPoint: rootRouteRef, }), ); diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index e75d93f56b..5241ae77bc 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-tech-radar +## 0.5.11 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- f69789330a: Fix an issue where the Radar is not updated when switching between different radars +- Updated dependencies + - @backstage/core-components@0.9.3 + - @backstage/core-plugin-api@1.0.1 + +## 0.5.11-next.2 + +### Patch Changes + +- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 +- 230ad0826f: Bump to using `@types/node` v16 +- Updated dependencies + - @backstage/core-components@0.9.3-next.2 + - @backstage/core-plugin-api@1.0.1-next.0 + ## 0.5.11-next.1 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 9d92b51682..560484e3c0 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.5.11-next.1", + "version": "0.5.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.9.3-next.1", - "@backstage/core-plugin-api": "^1.0.0", + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.17.0-next.2", - "@backstage/core-app-api": "^1.0.1-next.0", - "@backstage/dev-utils": "^1.0.1-next.0", - "@backstage/test-utils": "^1.0.1-next.1", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 7729ee6a15..b6f750865b 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-techdocs-backend +## 1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- Updated dependencies + - @backstage/backend-common@0.13.3-next.0 + - @backstage/integration@1.2.0-next.0 + - @backstage/plugin-techdocs-node@1.1.1-next.0 + +## 1.1.0 + +### Minor Changes + +- 733187987b: Removed an undocumented, broken behavior where `README.md` files would be copied to `index.md` if it did not exist, leading to broken links in the TechDocs UI. + + **WARNING**: If you notice 404s in TechDocs after updating, check to make sure that all markdown files referenced in your `mkdocs.yml`s' `nav` sections exist. The following configuration may be used to temporarily revert to the broken behavior. + + ```yaml + techdocs: + generator: + mkdocs: + legacyCopyReadmeMdToIndexMd: true + ``` + +### Patch Changes + +- ada4446733: Specify type of `visibilityPermission` property on collators and collator factories. +- 7762d54200: Fixed a bug affecting those with cache enabled that would result in empty content being cached if the first attempt to load a static asset from storage were made via a `HEAD` request, rather than a `GET` request. +- Updated dependencies + - @backstage/integration@1.1.0 + - @backstage/plugin-permission-common@0.6.0 + - @backstage/catalog-model@1.0.1 + - @backstage/plugin-search-common@0.3.3 + - @backstage/backend-common@0.13.2 + - @backstage/plugin-catalog-common@1.0.1 + - @backstage/plugin-techdocs-node@1.1.0 + - @backstage/catalog-client@1.0.1 + +## 1.1.0-next.2 + +### Minor Changes + +- bcf1a2496c: BREAKING: The default Techdocs behavior will no longer attempt to copy `docs/README.md` or `README.md` to `docs/index.md` (if not found). To retain this behavior in your instance, you can set the following config in your `app-config.yaml`: + + ```yaml + techdocs: + generator: + mkdocs: + legacyCopyReadmeMdToIndexMd: true + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.6.0-next.1 + - @backstage/backend-common@0.13.2-next.2 + - @backstage/integration@1.1.0-next.2 + - @backstage/plugin-techdocs-node@1.1.0-next.2 + ## 1.0.1-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 2087979d22..1a159a4d12 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -1,4 +1,6 @@ site_name: 'Example Documentation' +repo_url: https://github.com/backstage/backstage +edit_uri: edit/master/plugins/techdocs-backend/examples/documented-component/docs nav: - Home: index.md diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 8fdc80fe82..c88b970474 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "1.0.1-next.1", + "version": "1.1.1-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,21 +34,21 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.13.2-next.1", - "@backstage/catalog-client": "^1.0.1-next.0", - "@backstage/catalog-model": "^1.0.1-next.0", + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", - "@backstage/integration": "^1.1.0-next.1", - "@backstage/plugin-catalog-common": "^1.0.1-next.1", - "@backstage/plugin-permission-common": "^0.6.0-next.0", - "@backstage/plugin-search-common": "^0.3.3-next.1", - "@backstage/plugin-techdocs-node": "^1.0.1-next.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-catalog-common": "^1.0.1", + "@backstage/plugin-permission-common": "^0.6.0", + "@backstage/plugin-search-common": "^0.3.3", + "@backstage/plugin-techdocs-node": "^1.1.1-next.0", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "fs-extra": "10.0.1", + "fs-extra": "10.1.0", "knex": "^1.0.2", "lodash": "^4.17.21", "node-fetch": "^2.6.7", @@ -56,9 +56,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.23-next.1", - "@backstage/cli": "^0.17.0-next.1", - "@backstage/plugin-search-backend-node": "0.5.3-next.0", + "@backstage/backend-test-utils": "^0.1.24-next.0", + "@backstage/cli": "^0.17.1-next.0", + "@backstage/plugin-search-backend-node": "0.6.1-next.0", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-module-addons-contrib/.eslintrc.js b/plugins/techdocs-module-addons-contrib/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md new file mode 100644 index 0000000000..f95ed10409 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -0,0 +1,17 @@ +# @backstage/plugin-techdocs-module-addons-contrib + +## 0.1.0-next.0 + +### Minor Changes + +- 5f4dbd2b52: A package for contributed TechDocs addons. + + In this release it will introduce the ReportIssue addon, which lets you select text and open a GitHub/Gitlab issue. + +### Patch Changes + +- 075a9a067b: Updated the return type of `createTechDocsAddonExtension` to better reflect the fact that passing children to Addon components is not a valid use-case. +- Updated dependencies + - @backstage/integration@1.2.0-next.0 + - @backstage/integration-react@1.1.0-next.0 + - @backstage/plugin-techdocs-react@0.1.1-next.0 diff --git a/plugins/techdocs-module-addons-contrib/api-report.md b/plugins/techdocs-module-addons-contrib/api-report.md new file mode 100644 index 0000000000..076b05957c --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/api-report.md @@ -0,0 +1,34 @@ +## API Report File for "@backstage/plugin-techdocs-module-addons-contrib" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; + +// @public +export const ReportIssue: (props: ReportIssueProps) => JSX.Element | null; + +// @public (undocumented) +export type ReportIssueProps = { + debounceTime?: number; + templateBuilder?: ReportIssueTemplateBuilder; +}; + +// @public (undocumented) +export type ReportIssueTemplate = { + title: string; + body: string; +}; + +// @public (undocumented) +export type ReportIssueTemplateBuilder = ({ + selection, +}: { + selection: Selection; +}) => ReportIssueTemplate; + +// @public +export const techdocsModuleAddonsContribPlugin: BackstagePlugin<{}, {}>; +``` diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json new file mode 100644 index 0000000000..531e68ad7a --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/plugin-techdocs-module-addons-contrib", + "description": "Plugin module for contributed TechDocs Addons", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs-module-addons-contrib" + }, + "keywords": [ + "backstage", + "techdocs" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-techdocs-react": "^0.1.1-next.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "git-url-parse": "^11.6.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jest": "^26.0.7", + "@types/node": "^16.11.26", + "@types/react": "^16.13.1 || ^17.0.0", + "msw": "^0.35.0", + "cross-fetch": "^3.1.5" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx new file mode 100644 index 0000000000..5fff56ecb7 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; + +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; + +import { IssueLink } from './IssueLink'; + +const defaultProps = { + repository: { + type: 'github', + name: 'backstage', + owner: 'backstage', + protocol: 'https', + resource: 'github.com', + }, + template: { + title: 'Documentation feedback', + body: '## Documentation Feedback 📝', + }, +}; + +describe('FeedbackLink', () => { + const apiSpy = new MockAnalyticsApi(); + + it('Should open new issue tab', () => { + render( + wrapInTestApp( + + + , + ), + ); + + const link = screen.getByText(/Open new Github issue/); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('target', '_blank'); + const encodedTitle = encodeURIComponent(defaultProps.template.title); + const encodedBody = encodeURIComponent(defaultProps.template.body); + expect(link).toHaveAttribute( + 'href', + `https://github.com/backstage/backstage/issues/new?title=${encodedTitle}&body=${encodedBody}`, + ); + }); + + it('Should track click events', async () => { + render( + wrapInTestApp( + + + , + ), + ); + + fireEvent.click(screen.getByText(/Open new Github issue/)); + + await waitFor(() => { + expect(apiSpy.getEvents()[0]).toMatchObject({ + action: 'click', + subject: 'Open new Github issue', + }); + }); + }); +}); diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx new file mode 100644 index 0000000000..0a1a64218a --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import BugReportIcon from '@material-ui/icons/BugReport'; + +import { Link, GitHubIcon } from '@backstage/core-components'; + +import { ReportIssueTemplate, Repository } from './types'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'grid', + gridGap: theme.spacing(1), + gridAutoFlow: 'column', + justifyContent: 'center', + alignItems: 'center', + color: theme.palette.common.black, + fontSize: theme.typography.button.fontSize, + }, +})); + +type IssueLinkProps = { + template: ReportIssueTemplate; + repository: Repository; +}; + +const getIcon = ({ type }: Repository) => { + if (type === 'github') { + return GitHubIcon; + } + return BugReportIcon; +}; + +const getName = ({ type }: Repository) => { + if (type === 'github') { + return 'Github'; + } + return 'Gitlab'; +}; + +const getUrl = (repository: Repository, template: ReportIssueTemplate) => { + const { title, body } = template; + const encodedTitle = encodeURIComponent(title); + const encodedBody = encodeURIComponent(body); + const { protocol, resource, owner, name, type } = repository; + const encodedOwner = encodeURIComponent(owner); + const encodedName = encodeURIComponent(name); + + const url = `${protocol}://${resource}/${encodedOwner}/${encodedName}`; + if (type === 'github') { + return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`; + } + return `${url}/issues/new?[title]=${encodedTitle}&[body]=${encodedBody}`; +}; + +export const IssueLink = ({ template, repository }: IssueLinkProps) => { + const classes = useStyles(); + + const Icon = getIcon(repository); + const url = getUrl(repository, template); + + return ( + + Open new {getName(repository)} issue + + ); +}; diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx new file mode 100644 index 0000000000..2fe8d604e6 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; + +import { makeStyles, Portal, Paper } from '@material-ui/core'; + +import { useGitTemplate, useGitRepository } from './hooks'; +import { ReportIssueTemplateBuilder } from './types'; +import { + PAGE_MAIN_CONTENT_SELECTOR, + PAGE_FEEDBACK_LINK_SELECTOR, + ADDON_FEEDBACK_CONTAINER_ID, + ADDON_FEEDBACK_CONTAINER_SELECTOR, +} from './constants'; +import { IssueLink } from './IssueLink'; + +import { + useShadowRootElements, + useShadowRootSelection, +} from '@backstage/plugin-techdocs-react'; + +const useStyles = makeStyles(theme => ({ + root: { + transform: 'translate(-100%, -100%)', + position: 'absolute', + padding: theme.spacing(1), + zIndex: theme.zIndex.tooltip, + background: theme.palette.common.white, + }, +})); + +type Style = { + top: string; + left: string; +}; + +/** + * @public + */ +export type ReportIssueProps = { + debounceTime?: number; + templateBuilder?: ReportIssueTemplateBuilder; +}; + +/** + * Show report issue button when text is highlighted + */ +export const ReportIssueAddon = ({ + debounceTime = 500, + templateBuilder: buildTemplate, +}: ReportIssueProps) => { + const classes = useStyles(); + const [style, setStyle] = useState