Merge branch 'master' into feature/remove-aws-v2

Signed-off-by: Andrew Ochsner <andrew.ochsner@cognizant.com>
This commit is contained in:
Andrew Ochsner
2023-05-09 08:19:41 -05:00
294 changed files with 5613 additions and 478 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Correct command to create new plugins
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/theme': minor
---
Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': minor
---
Upgrade to aws-sdk v3 and include OpenSearch Serverless support
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-common': minor
'@backstage/plugin-scaffolder-react': minor
---
Add support for Markdown text blob outputs from templates
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Remove unnecessary join in the entity facets endpoint, exclude nulls
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/theme': patch
---
Fix accessibility issue with Backstage Table's header style
+39 -2
View File
@@ -220,52 +220,89 @@
"@backstage/plugin-vault": "0.1.11",
"@backstage/plugin-vault-backend": "0.3.0",
"@backstage/plugin-xcmetrics": "0.2.37",
"@backstage/plugin-analytics-module-ga4": "0.0.0"
"@backstage/plugin-analytics-module-ga4": "0.0.0",
"@backstage/plugin-devtools": "0.0.0",
"@backstage/plugin-devtools-backend": "0.0.0",
"@backstage/plugin-devtools-common": "0.0.0"
},
"changesets": [
"afraid-shirts-train",
"backend-like-a-tattoo",
"beige-tigers-matter",
"blue-eyes-run",
"brown-eyes-yell",
"brown-keys-raise",
"calm-snails-trade",
"calm-wolves-taste",
"chilled-goats-jump",
"chilled-pants-wonder",
"chilled-queens-wonder",
"chilly-fireants-type",
"chilly-taxis-shop",
"clean-lobsters-brake",
"cold-drinks-exercise",
"cold-laws-turn",
"cool-dingos-repair",
"create-app-1682437922",
"curly-rats-fold",
"curvy-months-appear",
"dry-boats-sniff",
"dull-wombats-stare",
"eighty-olives-live",
"extragavent-fast-fly",
"fair-suits-warn",
"fifty-grapes-explode",
"fifty-laws-count",
"five-tigers-whisper",
"fluffy-crabs-reply",
"fluffy-mirrors-happen",
"fresh-moons-unite",
"funny-trains-sniff",
"good-lions-approve",
"green-cheetahs-cover",
"healthy-ladybugs-chew",
"heavy-penguins-burn",
"honest-countries-deny",
"honest-turkeys-learn",
"hungry-foxes-divide",
"hungry-peas-sing",
"khaki-brooms-kiss",
"lazy-keys-work",
"mighty-bears-glow",
"mighty-cows-greet",
"modern-pumpkins-join",
"moody-lizards-beam",
"moody-shrimps-train",
"new-roses-fail",
"nine-ties-smoke",
"ninety-eggs-lie",
"ninety-mugs-sin",
"olive-turkeys-switch",
"polite-dingos-train",
"pretty-gifts-greet",
"quick-flies-guess",
"quiet-bikes-smash",
"quiet-stingrays-fly",
"rare-elephants-arrive",
"red-cheetahs-invite",
"rotten-chefs-jog",
"serious-hornets-remember",
"six-mails-shout",
"slimy-turkeys-return",
"soft-readers-exist",
"stupid-seas-stare",
"swift-fishes-run",
"tame-games-rule",
"thick-parents-pump",
"thick-pugs-rush",
"thin-ways-exist",
"thirty-jobs-sort",
"tough-moles-whisper",
"twelve-zebras-repair",
"two-gorillas-drop",
"unlucky-deers-teach",
"unlucky-plants-give",
"warm-lamps-tease"
"warm-lamps-tease",
"wet-dolphins-love"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': minor
---
The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added a persistent session store through the database
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-backend-module-github': minor
---
Implement events support for `GithubMultiOrgEntityProvider`
**BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
+1 -1
View File
@@ -1,5 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-github': major
'@backstage/plugin-catalog-backend-module-github': patch
---
Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
+1
View File
@@ -247,6 +247,7 @@ Olausson
Oldsberg
onboarding
Onboarding
OpenSearch
OpenShift
orgs
padding
+5 -1
View File
@@ -92,10 +92,14 @@ For those contributors who have earned write access to the repository, when a pu
## Code of Conduct
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
This project adheres to the [CNCF Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md
## Security Issues?
See [SECURITY](SECURITY.md).
## Accessibility
We encourage you to catch any accessibility issues already in the development phase of new features to Backstage, see our [Accessibility documentation](./docs/accessibility/index.md) for more details.
+60
View File
@@ -0,0 +1,60 @@
---
id: index
title: Backstage Accessibility
description: Documentation on Backstage Accessibility
---
In an effort to bake accessibility practices further into our process of building features for Backstage, we support automated CI tests for some of our core features in the OSS project: Software Catalog, Software Templates, Search, and TechDocs. As these are just a few of the many plugins out there, we encourage you to consider the accessibility implications of your work on Backstage in order to build a great experience for everyone.
## How to contribute
There are multiple ways to contribute to making Backstage more accessible, you'll find below a list of examples to help you get started.
### Run Lighthouse in CI on your plugin
If your plugin lives in the [Backstage main repository](https://github.com/backstage/backstage/) you can modify the [urls in the Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) to run the Lighthouse checks on urls where your plugin exists as well. E.g.
```diff
ci: {
collect: {
url: [
+ /** Your plugin paths */
+ 'http://localhost:3000/your-plugin-path,
/** Software Catalog */
'http://localhost:3000/catalog',
'http://localhost:3000/catalog-import',
'http://localhost:3000/catalog/default/component/backstage',
...
],
settings: {
...
},
...
},
assert: {
...
},
},
```
To make sure the [Accessibility Github workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16).
### Run the Lighthouse CLI locally when developing new features
If you want to use the Lighthouse CLI and run the checks based on the config you can use the following command:
```
yarn dlx @lhci/cli@0.11.x autorun
```
> Note: running this command will use the [Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) so make sure to adjust it to your needs if needed.
### Use Lighthouse Github Action on your own repo
If your Backstage plugin lives outside of the [Backstage main repository](https://github.com/backstage/backstage/), and you use GitHub Actions for continuous integration, we encourage you to add and modify the [Accessibility GitHub workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) to your needs.
### Report identified issues
Its important to remember that automated checks can only catch a small number of accessibility issues, therefore we also encourage you to conduct manual testing of your plugins using Assistive technology (screen readers, alternative navigation, and screen magnifiers are a few examples).
If you have identified accessibility issues and dont have time to contribute a fix, please open an issue over at [Backstage Issues](https://github.com/backstage/backstage/issues) to let us know.
@@ -510,10 +510,8 @@ take a look at, or you can
Each individual step can output some variables that can be used in the
scaffolder frontend for after the job is finished. This is useful for things
like linking to the entity that has been created with the backend, and also
linking to the created repository.
The main two that are used are the following:
like linking to the entity that has been created with the backend, linking
to the created repository, or showing Markdown text blobs.
```yaml
output:
@@ -523,6 +521,10 @@ output:
- title: Open in catalog
icon: catalog
entityRef: ${{ steps['register'].output.entityRef }} # link to the entity that has been ingested to the catalog
text:
- title: More information
content: |
**Entity URL:** `${{ steps['publish'].output.remoteUrl }}`
```
## The templating syntax
+72
View File
@@ -65,6 +65,42 @@ export default async function createPlugin(
}
```
Alternatively, if you wish to ingest data from multiple GitHub organizations you can use
the `GithubMultiOrgEntityProvider` instead. Note that by default, this provider will namespace
groups according to the org they originate from to avoid potential name duplicates:
```ts title="packages/backend/src/plugins/catalog.ts"
/* highlight-add-next-line */
import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
/* highlight-add-start */
// The GitHub URL below needs to match a configured integrations.github entry
// specified in your app-config.
builder.addEntityProvider(
GithubMultiOrgEntityProvider.fromConfig(env.config, {
id: 'production',
githubUrl: 'https://github.com',
// Set the following to list the GitHub orgs you wish to ingest from. You can
// also omit this option to ingest all orgs accessible by your GitHub integration
orgs: ['org-a', 'org-b'],
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 60 },
timeout: { minutes: 15 },
}),
}),
);
/* highlight-add-end */
// ..
}
```
## Installation with Events Support
Please follow the installation instructions at
@@ -110,6 +146,42 @@ export default async function createPlugin(
}
```
Or, alternatively, if using the `GithubMultiOrgEntityProvider`:
```ts title="packages/backend/src/plugins/catalog.ts"
/* highlight-add-next-line */
import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
/* highlight-add-start */
// The GitHub URL below needs to match a configured integrations.github entry
// specified in your app-config.
builder.addEntityProvider(
GithubMultiOrgEntityProvider.fromConfig(env.config, {
id: 'production',
githubUrl: 'https://github.com',
// Set the following to list the GitHub orgs you wish to ingest from. You can
// also omit this option to ingest all orgs accessible by your GitHub integration
orgs: ['org-a', 'org-b'],
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 60 },
timeout: { minutes: 15 },
}),
// Pass in the eventBroker to allow this provider to subscribe to GitHub events
eventBroker: env.eventBroker,
}),
);
/* highlight-add-end */
// ..
}
```
You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
The webhook will need to be configured to forward `organization`,`team` and `membership` events.
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
---
title: xkcd comic
author: Vity
authorUrl: https://github.com/Vity01
category: Humor
description: xkcd comic within Backstage
documentation: https://github.com/Vity01/backstage-xkcd/
iconUrl: https://raw.githubusercontent.com/Vity01/backstage-xkcd/main/docs/xkcd.svg
npmPackageName: 'backstage-plugin-xkcd'
addedDate: '2023-05-05'
+2 -1
View File
@@ -408,6 +408,7 @@
"label": "Core Services",
"items": ["backend-system/core-services/index"]
}
]
],
"Accessibility": ["accessibility/index"]
}
}
+1 -1
View File
@@ -47,7 +47,7 @@
"@types/react": "^17",
"@types/react-dom": "^17"
},
"version": "1.14.0-next.1",
"version": "1.14.0-next.2",
"dependencies": {
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3"
+11
View File
@@ -1,5 +1,16 @@
# @backstage/app-defaults
## 1.3.1-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/core-app-api@1.8.0-next.1
- @backstage/core-plugin-api@1.5.1
- @backstage/plugin-permission-react@0.4.12
## 1.3.1-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
"version": "1.3.1-next.1",
"version": "1.3.1-next.2",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
+68
View File
@@ -1,5 +1,73 @@
# example-app
## 0.2.83-next.2
### Patch Changes
- Updated dependencies
- @backstage/plugin-devtools@0.1.0-next.0
- @backstage/plugin-kubernetes@0.9.0-next.2
- @backstage/plugin-home@0.5.2-next.2
- @backstage/theme@0.3.0-next.0
- @backstage/plugin-badges@0.2.42-next.2
- @backstage/plugin-tech-radar@0.6.4-next.2
- @backstage/plugin-scaffolder-react@1.4.0-next.2
- @backstage/plugin-explore@0.4.3-next.2
- @backstage/plugin-org@0.6.8-next.2
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-search-react@1.6.0-next.2
- @backstage/plugin-azure-devops@0.3.0-next.2
- @backstage/plugin-stack-overflow@0.1.15-next.2
- @backstage/app-defaults@1.3.1-next.2
- @backstage/cli@0.22.7-next.0
- @backstage/integration-react@1.1.13-next.2
- @backstage/plugin-airbrake@0.3.18-next.2
- @backstage/plugin-api-docs@0.9.3-next.2
- @backstage/plugin-azure-sites@0.1.7-next.2
- @backstage/plugin-catalog-graph@0.2.30-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/plugin-circleci@0.3.18-next.2
- @backstage/plugin-cloudbuild@0.3.18-next.2
- @backstage/plugin-code-coverage@0.2.11-next.2
- @backstage/plugin-cost-insights@0.12.7-next.2
- @backstage/plugin-dynatrace@5.0.0-next.2
- @backstage/plugin-entity-feedback@0.2.1-next.2
- @backstage/plugin-gcalendar@0.3.14-next.1
- @backstage/plugin-gcp-projects@0.3.37-next.1
- @backstage/plugin-github-actions@0.5.18-next.2
- @backstage/plugin-gocd@0.1.24-next.2
- @backstage/plugin-graphiql@0.2.50-next.1
- @backstage/plugin-jenkins@0.7.17-next.2
- @backstage/plugin-kafka@0.3.18-next.2
- @backstage/plugin-lighthouse@0.4.3-next.2
- @backstage/plugin-linguist@0.1.3-next.2
- @backstage/plugin-microsoft-calendar@0.1.3-next.1
- @backstage/plugin-newrelic@0.3.36-next.1
- @backstage/plugin-octopus-deploy@0.2.0-next.2
- @backstage/plugin-pagerduty@0.5.11-next.2
- @backstage/plugin-playlist@0.1.9-next.2
- @backstage/plugin-puppetdb@0.1.1-next.2
- @backstage/plugin-rollbar@0.4.18-next.2
- @backstage/plugin-scaffolder@1.13.1-next.2
- @backstage/plugin-search@1.3.0-next.2
- @backstage/plugin-sentry@0.5.3-next.2
- @backstage/plugin-shortcuts@0.3.10-next.2
- @backstage/plugin-stackstorm@0.1.2-next.1
- @backstage/plugin-tech-insights@0.3.10-next.2
- @backstage/plugin-techdocs@1.6.2-next.2
- @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2
- @backstage/plugin-techdocs-react@1.1.6-next.1
- @backstage/plugin-todo@0.2.20-next.2
- @backstage/plugin-user-settings@0.7.3-next.2
- @backstage/plugin-apache-airflow@0.2.11-next.1
- @backstage/plugin-catalog-import@0.9.8-next.2
- @backstage/plugin-newrelic-dashboard@0.2.11-next.2
- @backstage/config@1.0.7
- @backstage/core-app-api@1.8.0-next.1
- @backstage/core-plugin-api@1.5.1
- @backstage/plugin-permission-react@0.4.12
- @internal/plugin-catalog-customized@0.0.10-next.2
## 0.2.83-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-app",
"version": "0.2.83-next.1",
"version": "0.2.83-next.2",
"private": true,
"backstage": {
"role": "frontend"
+10
View File
@@ -1,5 +1,15 @@
# example-backend-next
## 0.0.11-next.2
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog-backend@1.9.1-next.2
- @backstage/plugin-kubernetes-backend@0.11.0-next.2
- @backstage/plugin-search-backend@1.3.1-next.2
- @backstage/plugin-scaffolder-backend@1.13.2-next.2
## 0.0.11-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
"version": "0.0.11-next.1",
"version": "0.0.11-next.2",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -1,5 +1,12 @@
# @backstage/backend-openapi-utils
## 0.0.2-next.1
### Patch Changes
- fe16bd39e83: Use permalinks for links including a line number reference
- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
## 0.0.2-next.0
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
"version": "0.0.2-next.0",
"version": "0.0.2-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+17
View File
@@ -1,5 +1,22 @@
# example-backend
## 0.2.83-next.2
### Patch Changes
- Updated dependencies
- @backstage/plugin-devtools-backend@0.1.0-next.0
- @backstage/plugin-catalog-backend@1.9.1-next.2
- @backstage/plugin-badges-backend@0.2.0-next.2
- @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2
- @backstage/plugin-kubernetes-backend@0.11.0-next.2
- @backstage/plugin-auth-backend@0.18.3-next.2
- @backstage/plugin-search-backend@1.3.1-next.2
- example-app@0.2.83-next.2
- @backstage/plugin-scaffolder-backend@1.13.2-next.2
- @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2
- @backstage/config@1.0.7
## 0.2.83-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.83-next.1",
"version": "0.2.83-next.2",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/core-components
## 0.13.1-next.1
### Patch Changes
- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/config@1.0.7
- @backstage/core-plugin-api@1.5.1
## 0.13.1-next.0
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
"version": "0.13.1-next.0",
"version": "0.13.1-next.1",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -105,7 +105,6 @@ const StyledMTableHeader = withStyles(
borderTop: `1px solid ${theme.palette.grey.A100}`,
borderBottom: `1px solid ${theme.palette.grey.A100}`,
// withStyles hasn't a generic overload for theme
color: (theme as BackstageTheme).palette.textSubtle,
fontWeight: theme.typography.fontWeightBold,
position: 'static',
wordBreak: 'normal',
+6
View File
@@ -1,5 +1,11 @@
# @backstage/create-app
## 0.5.1-next.2
### Patch Changes
- 1d5e42655cd: Correct command to create new plugins
## 0.5.1-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
"version": "0.5.1-next.1",
"version": "0.5.1-next.2",
"publishConfig": {
"access": "public"
},
@@ -4,6 +4,6 @@ This is where your own plugins and their associated modules live, each in a
separate folder of its own.
If you want to create a new plugin here, go to your project root directory, run
the command `yarn backstage-cli create`, and follow the on-screen instructions.
the command `yarn new`, and follow the on-screen instructions.
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!
+14
View File
@@ -1,5 +1,19 @@
# @backstage/dev-utils
## 1.0.15-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/app-defaults@1.3.1-next.2
- @backstage/integration-react@1.1.13-next.2
- @backstage/test-utils@1.3.1-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-app-api@1.8.0-next.1
- @backstage/core-plugin-api@1.5.1
## 1.0.15-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "1.0.15-next.1",
"version": "1.0.15-next.2",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
+7
View File
@@ -1,5 +1,12 @@
# e2e-test
## 0.2.3-next.2
### Patch Changes
- Updated dependencies
- @backstage/create-app@0.5.1-next.2
## 0.2.3-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.2.3-next.1",
"version": "0.2.3-next.2",
"private": true,
"backstage": {
"role": "cli"
+10
View File
@@ -1,5 +1,15 @@
# @backstage/integration-react
## 1.1.13-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/config@1.0.7
- @backstage/core-plugin-api@1.5.1
## 1.1.13-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
"version": "1.1.13-next.1",
"version": "1.1.13-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -1,5 +1,23 @@
# techdocs-cli-embedded-app
## 0.2.82-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/app-defaults@1.3.1-next.2
- @backstage/cli@0.22.7-next.0
- @backstage/integration-react@1.1.13-next.2
- @backstage/test-utils@1.3.1-next.2
- @backstage/plugin-catalog@1.11.0-next.2
- @backstage/plugin-techdocs@1.6.2-next.2
- @backstage/plugin-techdocs-react@1.1.6-next.1
- @backstage/config@1.0.7
- @backstage/core-app-api@1.8.0-next.1
- @backstage/core-plugin-api@1.5.1
## 0.2.82-next.1
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "techdocs-cli-embedded-app",
"version": "0.2.82-next.1",
"version": "0.2.82-next.2",
"private": true,
"backstage": {
"role": "frontend"
+11
View File
@@ -1,5 +1,16 @@
# @backstage/test-utils
## 1.3.1-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/config@1.0.7
- @backstage/core-app-api@1.8.0-next.1
- @backstage/core-plugin-api@1.5.1
- @backstage/plugin-permission-react@0.4.12
## 1.3.1-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "1.3.1-next.1",
"version": "1.3.1-next.2",
"publishConfig": {
"access": "public"
},
+10
View File
@@ -1,5 +1,15 @@
# @backstage/theme
## 0.3.0-next.0
### Minor Changes
- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
### Patch Changes
- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
## 0.2.19
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.2.19",
"version": "0.3.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
-5
View File
@@ -159,7 +159,6 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
head: {
wordBreak: 'break-word',
overflow: 'hidden',
color: theme.palette.textSubtle,
fontWeight: 'normal',
lineHeight: '1',
},
@@ -198,14 +197,10 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
'&:hover': {
color: 'inherit',
},
'&:focus': {
color: 'inherit',
},
},
// Bold font for highlighting selected column
active: {
fontWeight: 'bold',
color: 'inherit',
},
},
MuiListItemText: {
+2 -2
View File
@@ -33,7 +33,7 @@ export const lightTheme = createTheme({
ok: '#1DB954',
warning: '#FF9800',
error: '#E22134',
running: '#2E77D0',
running: '#1F5493',
pending: '#FFED51',
aborted: '#757575',
},
@@ -48,7 +48,7 @@ export const lightTheme = createTheme({
},
},
primary: {
main: '#2E77D0',
main: '#1F5493',
},
banner: {
info: '#2E77D0',
+12
View File
@@ -1,5 +1,17 @@
# @backstage/plugin-adr
## 0.5.1-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-search-react@1.6.0-next.2
- @backstage/integration-react@1.1.13-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.5.1-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
"version": "0.5.1-next.1",
"version": "0.5.1-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+12
View File
@@ -1,5 +1,17 @@
# @backstage/plugin-airbrake
## 0.3.18-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/dev-utils@1.0.15-next.2
- @backstage/test-utils@1.3.1-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.3.18-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
"version": "0.3.18-next.1",
"version": "0.3.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-allure
## 0.1.34-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.1.34-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
"version": "0.1.34-next.1",
"version": "0.1.34-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-ga
## 0.1.29-next.1
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/config@1.0.7
- @backstage/core-plugin-api@1.5.1
## 0.1.29-next.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga",
"version": "0.1.29-next.0",
"version": "0.1.29-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-ga4
## 0.1.0-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/config@1.0.7
- @backstage/core-plugin-api@1.5.1
## 0.1.0-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga4",
"version": "0.1.0-next.1",
"version": "0.1.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-apache-airflow
## 0.2.11-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.13.1-next.1
- @backstage/core-plugin-api@1.5.1
## 0.2.11-next.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
"version": "0.2.11-next.0",
"version": "0.2.11-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-api-docs
## 0.9.3-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog@1.11.0-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.9.3-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
"version": "0.9.3-next.1",
"version": "0.9.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-apollo-explorer
## 0.1.11-next.1
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/core-plugin-api@1.5.1
## 0.1.11-next.0
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apollo-explorer",
"version": "0.1.11-next.0",
"version": "0.1.11-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-auth-backend
## 0.18.3-next.2
### Patch Changes
- 3ffcdac7d07: Added a persistent session store through the database
- Updated dependencies
- @backstage/config@1.0.7
## 0.18.3-next.1
### Patch Changes
@@ -0,0 +1,52 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
// See https://github.com/gx0r/connect-session-knex
// Modeled loosely after https://github.com/gx0r/connect-session-knex/blob/4e0e36a9afbb13c3000a89f5e341f2d2d4339a02/lib/index.js#L114
// For simplicity we always make the session a string
// Do NOT change around this table or column names; the connect-session-knex library makes assumptions about them
await knex.schema.createTable('sessions', table => {
table.comment('Session data');
table.string('sid').primary().notNullable().comment('ID of the session');
table
.text('sess', 'longtext')
.notNullable()
.comment('Session data, JSON serialized');
table
.timestamp('expired')
.notNullable()
.comment('The point in time when the session expires');
table.index('sid', 'sessions_sid_idx');
table.index('expired', 'sessions_expired_idx');
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.alterTable('sessions', table => {
table.dropIndex([], 'sessions_sid_idx');
table.dropIndex([], 'sessions_expired_idx');
});
await knex.schema.dropTable('sessions');
};
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
"version": "0.18.3-next.1",
"version": "0.18.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -44,6 +44,7 @@
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
"compression": "^1.7.4",
"connect-session-knex": "^3.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1",
@@ -0,0 +1,77 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DatabaseManager,
PluginDatabaseManager,
resolvePackagePath,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Knex } from 'knex';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-auth-backend',
'migrations',
);
/**
* Ensures that a database connection is established exactly once and only when
* asked for, and runs migrations.
*/
export class AuthDatabase {
readonly #database: PluginDatabaseManager;
#promise: Promise<Knex> | undefined;
static create(database: PluginDatabaseManager): AuthDatabase {
return new AuthDatabase(database);
}
/** @internal */
static forTesting(): AuthDatabase {
const config = new ConfigReader({
backend: {
database: {
client: 'better-sqlite3',
connection: ':memory:',
useNullAsDefault: true,
},
},
});
const database = DatabaseManager.fromConfig(config).forPlugin('auth');
return new AuthDatabase(database);
}
static async runMigrations(knex: Knex): Promise<void> {
await knex.migrate.latest({
directory: migrationsDir,
});
}
private constructor(database: PluginDatabaseManager) {
this.#database = database;
}
get(): Promise<Knex> {
this.#promise ??= this.#database.getClient().then(async client => {
if (!this.#database.migrations?.skip) {
await AuthDatabase.runMigrations(client);
}
return client;
});
return this.#promise;
}
}
@@ -14,33 +14,10 @@
* limitations under the License.
*/
import Knex, { Knex as KnexType } from 'knex';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DateTime } from 'luxon';
function createDatabaseManager(
client: KnexType,
skipMigrations: boolean = false,
) {
return {
getClient: async () => client,
migrations: {
skip: skipMigrations,
},
};
}
function createDB() {
const knex = Knex({
client: 'better-sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return knex;
}
import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { TestDatabases } from '@backstage/backend-test-utils';
const keyBase = {
use: 'sig',
@@ -48,95 +25,107 @@ const keyBase = {
alg: 'Base64',
} as const;
jest.setTimeout(60_000);
describe('DatabaseKeyStore', () => {
it('should store a key', async () => {
const client = createDB();
const store = await DatabaseKeyStore.create({
database: createDatabaseManager(client),
});
const key = {
kid: '123',
...keyBase,
};
await expect(store.listKeys()).resolves.toEqual({ items: [] });
await store.addKey(key);
const { items } = await store.listKeys();
expect(items).toEqual([{ createdAt: expect.anything(), key }]);
expect(
Math.abs(
DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
),
).toBeLessThan(10);
const databases = TestDatabases.create({
ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
it('should remove stored keys', async () => {
const client = createDB();
const store = await DatabaseKeyStore.create({
database: createDatabaseManager(client),
});
it.each(databases.eachSupportedId())(
'should store a key, %p',
async databaseId => {
const knex = await databases.init(databaseId);
await AuthDatabase.runMigrations(knex);
const key1 = { kid: '1', ...keyBase };
const key2 = { kid: '2', ...keyBase };
const key3 = { kid: '3', ...keyBase };
const store = new DatabaseKeyStore(knex);
await store.addKey(key1);
await store.addKey(key2);
await store.addKey(key3);
const key = {
kid: '123',
...keyBase,
};
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key1, createdAt: expect.anything() },
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
await expect(store.listKeys()).resolves.toEqual({ items: [] });
await store.addKey(key);
store.removeKeys(['1']);
const { items } = await store.listKeys();
expect(items).toEqual([{ createdAt: expect.anything(), key }]);
expect(
Math.abs(
DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
),
).toBeLessThan(10);
},
);
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
it.each(databases.eachSupportedId())(
'should remove stored keys, %p',
async databaseId => {
const knex = await databases.init(databaseId);
await AuthDatabase.runMigrations(knex);
store.removeKeys(['1', '2']);
const store = new DatabaseKeyStore(knex);
await expect(store.listKeys()).resolves.toEqual({
items: [{ key: key3, createdAt: expect.anything() }],
});
const key1 = { kid: '1', ...keyBase };
const key2 = { kid: '2', ...keyBase };
const key3 = { kid: '3', ...keyBase };
store.removeKeys([]);
await store.addKey(key1);
await store.addKey(key2);
await store.addKey(key3);
await expect(store.listKeys()).resolves.toEqual({
items: [{ key: key3, createdAt: expect.anything() }],
});
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key1, createdAt: expect.anything() },
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
store.removeKeys(['3', '4']);
await store.removeKeys(['1']);
await expect(store.listKeys()).resolves.toEqual({
items: [],
});
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
await store.addKey(key1);
await store.addKey(key2);
await store.addKey(key3);
await store.removeKeys(['1', '2']);
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key1, createdAt: expect.anything() },
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
await expect(store.listKeys()).resolves.toEqual({
items: [{ key: key3, createdAt: expect.anything() }],
});
store.removeKeys(['1', '2', '3']);
await store.removeKeys([]);
await expect(store.listKeys()).resolves.toEqual({
items: [],
});
});
await expect(store.listKeys()).resolves.toEqual({
items: [{ key: key3, createdAt: expect.anything() }],
});
await store.removeKeys(['3', '4']);
await expect(store.listKeys()).resolves.toEqual({
items: [],
});
await store.addKey(key1);
await store.addKey(key2);
await store.addKey(key3);
await expect(store.listKeys()).resolves.toEqual({
items: [
{ key: key1, createdAt: expect.anything() },
{ key: key2, createdAt: expect.anything() },
{ key: key3, createdAt: expect.anything() },
],
});
await store.removeKeys(['1', '2', '3']);
await expect(store.listKeys()).resolves.toEqual({
items: [],
});
},
);
});
@@ -14,19 +14,10 @@
* limitations under the License.
*/
import {
PluginDatabaseManager,
resolvePackagePath,
} from '@backstage/backend-common';
import { Knex } from 'knex';
import { DateTime } from 'luxon';
import { AnyJWK, KeyStore, StoredKey } from './types';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-auth-backend',
'migrations',
);
const TABLE = 'signing_keys';
type Row = {
@@ -35,10 +26,6 @@ type Row = {
key: string;
};
type Options = {
database: PluginDatabaseManager;
};
const parseDate = (date: string | Date) => {
const parsedDate =
typeof date === 'string'
@@ -55,24 +42,7 @@ const parseDate = (date: string | Date) => {
};
export class DatabaseKeyStore implements KeyStore {
static async create(options: Options): Promise<DatabaseKeyStore> {
const { database } = options;
const client = await database.getClient();
if (!database.migrations?.skip) {
await client.migrate.latest({
directory: migrationsDir,
});
}
return new DatabaseKeyStore(client);
}
private readonly client: Knex;
private constructor(client: Knex) {
this.client = client;
}
constructor(private readonly client: Knex) {}
async addKey(key: AnyJWK): Promise<void> {
await this.client<Row>(TABLE).insert({
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { MemoryKeyStore } from './MemoryKeyStore';
import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
import { KeyStores } from './KeyStores';
import { MemoryKeyStore } from './MemoryKeyStore';
import { getVoidLogger } from '@backstage/backend-common';
describe('KeyStores', () => {
const defaultConfigOptions = {
@@ -34,7 +34,10 @@ describe('KeyStores', () => {
it('reads auth section from config', async () => {
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
const keyStore = await KeyStores.fromConfig(defaultConfig);
const keyStore = await KeyStores.fromConfig(defaultConfig, {
logger: getVoidLogger(),
database: AuthDatabase.forTesting(),
});
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
@@ -46,18 +49,10 @@ describe('KeyStores', () => {
});
it('can handle without auth config', async () => {
const config = new ConfigReader({
backend: {
database: {
client: 'better-sqlite3',
connection: ':memory:',
},
},
const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
logger: getVoidLogger(),
database: AuthDatabase.forTesting(),
});
const database =
DatabaseManager.fromConfig(config).forPlugin('auth-backend');
const keyStore = await KeyStores.fromConfig(config, { database });
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
});
@@ -82,7 +77,10 @@ describe('KeyStores', () => {
},
};
const config = new ConfigReader(configOptions);
const keyStore = await KeyStores.fromConfig(config);
const keyStore = await KeyStores.fromConfig(config, {
logger: getVoidLogger(),
database: AuthDatabase.forTesting(),
});
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
expect(createSpy).toHaveBeenCalledWith(
+9 -16
View File
@@ -14,20 +14,20 @@
* limitations under the License.
*/
import { Logger } from 'winston';
import { pickBy } from 'lodash';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { MemoryKeyStore } from './MemoryKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
logger?: Logger;
database?: PluginDatabaseManager;
logger: Logger;
database: AuthDatabase;
};
export class KeyStores {
@@ -37,23 +37,16 @@ export class KeyStores {
*
* @returns a KeyStore store
*/
static async fromConfig(
config: Config,
options?: Options,
): Promise<KeyStore> {
const { logger, database } = options ?? {};
static async fromConfig(config: Config, options: Options): Promise<KeyStore> {
const { logger, database } = options;
const ks = config.getOptionalConfig('auth.keyStore');
const provider = ks?.getOptionalString('provider') ?? 'database';
logger?.info(`Configuring "${provider}" as KeyStore provider`);
logger.info(`Configuring "${provider}" as KeyStore provider`);
if (provider === 'database') {
if (!database) {
throw new Error('This KeyStore provider requires a database');
}
return await DatabaseKeyStore.create({ database });
return new DatabaseKeyStore(await database.get());
}
if (provider === 'memory') {
@@ -0,0 +1,76 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex } from 'knex';
import { TestDatabases } from '@backstage/backend-test-utils';
import fs from 'fs';
const migrationsDir = `${__dirname}/../migrations`;
const migrationsFiles = fs.readdirSync(migrationsDir).sort();
async function migrateUpOnce(knex: Knex): Promise<void> {
await knex.migrate.up({ directory: migrationsDir });
}
async function migrateDownOnce(knex: Knex): Promise<void> {
await knex.migrate.down({ directory: migrationsDir });
}
async function migrateUntilBefore(knex: Knex, target: string): Promise<void> {
const index = migrationsFiles.indexOf(target);
if (index === -1) {
throw new Error(`Migration ${target} not found`);
}
for (let i = 0; i < index; i++) {
await migrateUpOnce(knex);
}
}
jest.setTimeout(60_000);
describe('migrations', () => {
const databases = TestDatabases.create({
ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
it.each(databases.eachSupportedId())(
'20230428155633_sessions.js, %p',
async databaseId => {
const knex = await databases.init(databaseId);
await migrateUntilBefore(knex, '20230428155633_sessions.js');
await migrateUpOnce(knex);
// Ensure that large cookies are supported
const data = `{"cookie":"${'a'.repeat(100_000)}"}`;
await knex
.insert({ sid: 'abc', expired: knex.fn.now(), sess: data })
.into('sessions');
await knex
.insert({ sid: 'def', expired: knex.fn.now(), sess: data })
.into('sessions');
await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([
{ sid: 'abc', expired: expect.anything(), sess: data },
{ sid: 'def', expired: expect.anything(), sess: data },
]);
await migrateDownOnce(knex);
await knex.destroy();
},
);
});
+12 -2
View File
@@ -32,9 +32,11 @@ import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { createOidcRouter, TokenFactory, KeyStores } from '../identity';
import session from 'express-session';
import connectSessionKnex from 'connect-session-knex';
import passport from 'passport';
import { Minimatch } from 'minimatch';
import { CatalogAuthResolverContext } from '../lib/resolvers';
import { AuthDatabase } from '../database/AuthDatabase';
/** @public */
export type ProviderFactories = { [s: string]: AuthProviderFactory };
@@ -70,7 +72,11 @@ export async function createRouter(
const appUrl = config.getString('app.baseUrl');
const authUrl = await discovery.getExternalBaseUrl('auth');
const keyStore = await KeyStores.fromConfig(config, { logger, database });
const authDb = AuthDatabase.create(database);
const keyStore = await KeyStores.fromConfig(config, {
logger,
database: authDb,
});
const keyDurationSeconds = 3600;
const tokenIssuer = new TokenFactory({
@@ -84,14 +90,18 @@ export async function createRouter(
const secret = config.getOptionalString('auth.session.secret');
if (secret) {
router.use(cookieParser(secret));
// TODO: Configure the server-side session storage. The default MemoryStore is not designed for production
const enforceCookieSSL = authUrl.startsWith('https');
const KnexSessionStore = connectSessionKnex(session);
router.use(
session({
secret,
saveUninitialized: false,
resave: false,
cookie: { secure: enforceCookieSSL ? 'auto' : false },
store: new KnexSessionStore({
createtable: false,
knex: await authDb.get(),
}),
}),
);
router.use(passport.initialize());
+14
View File
@@ -1,5 +1,19 @@
# @backstage/plugin-azure-devops
## 0.3.0-next.2
### Minor Changes
- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.2.9-next.1
### Patch Changes
+1 -1
View File
@@ -70,7 +70,7 @@ dev.azure.com/project: <project-name>
dev.azure.com/build-definition: <build-definition-name>
```
In this case `<project-name>` will be the name of your Team Project and `<build-definition-name>` will be the name of the Build Definition you would like to see Builds for. If the Build Definition name has spaces in it make sure to put quotes around it
In this case `<project-name>` will be the name of your Team Project and `<build-definition-name>` will be the name of the Build Definition you would like to see Builds for, and it's possible to add more Builds separated by a comma. If the Build Definition name has spaces in it make sure to put quotes around it.
### Azure Pipelines Component
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops",
"version": "0.2.9-next.1",
"version": "0.3.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -120,7 +120,23 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
queryString.append('repoName', repoName);
}
if (definitionName) {
queryString.append('definitionName', definitionName);
const definitionNames = definitionName.split(',');
if (definitionNames.length > 1) {
const buildRuns: BuildRun[] = [];
for (const name of definitionNames) {
queryString.set('definitionName', name.trim());
if (options?.top) {
queryString.set('top', options.top.toString());
}
const urlSegment = `builds/${encodeURIComponent(
projectName,
)}?${queryString}`;
const items = await this.get<BuildRun[]>(urlSegment);
buildRuns.push(...items);
}
return { items: buildRuns };
}
queryString.append('definitionName', definitionName.trim());
}
if (options?.top) {
queryString.append('top', options.top.toString());
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-azure-sites
## 0.1.7-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.1.7-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-sites",
"version": "0.1.7-next.1",
"version": "0.1.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+15
View File
@@ -1,5 +1,20 @@
# @backstage/plugin-badges-backend
## 0.2.0-next.2
### Minor Changes
- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
**BREAKING**: `createRouter` now require that `tokenManager`, `logger`, `identityApi` and `database`, are passed in as options.
### Patch Changes
- Updated dependencies
- @backstage/config@1.0.7
## 0.1.39-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges-backend",
"description": "A Backstage backend plugin that generates README badges for your entities",
"version": "0.1.39-next.1",
"version": "0.2.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+16
View File
@@ -1,5 +1,21 @@
# @backstage/plugin-badges
## 0.2.42-next.2
### Patch Changes
- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
**BREAKING**: `createRouter` now require that `tokenManager`, `logger`, `identityApi` and `database`, are passed in as options.
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.2.42-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges",
"description": "A Backstage plugin that generates README badges for your entities",
"version": "0.2.42-next.1",
"version": "0.2.42-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+13
View File
@@ -1,5 +1,18 @@
# @backstage/plugin-bazaar
## 0.2.8-next.2
### Patch Changes
- 900880ab7c3: Fixed `validateDOMNesting` warnings
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/cli@0.22.7-next.0
- @backstage/plugin-catalog@1.11.0-next.2
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.2.8-next.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bazaar",
"version": "0.2.8-next.1",
"version": "0.2.8-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-bitrise
## 0.1.45-next.2
### Patch Changes
- Updated dependencies
- @backstage/theme@0.3.0-next.0
- @backstage/core-components@0.13.1-next.1
- @backstage/plugin-catalog-react@1.6.0-next.2
- @backstage/core-plugin-api@1.5.1
## 0.1.45-next.1
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-bitrise",
"description": "A Backstage plugin that integrates towards Bitrise",
"version": "0.1.45-next.1",
"version": "0.1.45-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend-module-aws
## 0.1.19-next.2
### Patch Changes
- Updated dependencies
- @backstage/plugin-kubernetes-common@0.6.3-next.0
- @backstage/config@1.0.7
## 0.1.19-next.1
### Patch Changes
@@ -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.19-next.1",
"version": "0.1.19-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -1,5 +1,20 @@
# @backstage/plugin-catalog-backend-module-github
## 0.3.0-next.2
### Minor Changes
- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider`
**BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
### Patch Changes
- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
- Updated dependencies
- @backstage/plugin-catalog-backend@1.9.1-next.2
- @backstage/config@1.0.7
## 0.2.8-next.1
### Patch Changes
@@ -10,6 +10,7 @@ import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { EventBroker } from '@backstage/plugin-events-node';
import { EventParams } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { GithubCredentialsProvider } from '@backstage/integration';
@@ -158,6 +159,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
// @public
export interface GithubMultiOrgEntityProviderOptions {
eventBroker?: EventBroker;
githubCredentialsProvider?: GithubCredentialsProvider;
githubUrl: string;
id: string;
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-github",
"description": "A Backstage catalog backend module that helps integrate towards GitHub",
"version": "0.2.8-next.1",
"version": "0.3.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,6 +21,7 @@ import { graphql as graphqlMsw } from 'msw';
import { setupServer } from 'msw/node';
import { TeamTransformer, UserTransformer } from './defaultTransformers';
import {
getOrganizationsFromUser,
getOrganizationTeams,
getOrganizationUsers,
getTeamMembers,
@@ -448,6 +449,37 @@ describe('github', () => {
});
});
describe('getOrganizationsFromUser', () => {
it('reads orgs from user', async () => {
const input: QueryResponse = {
user: {
organizations: {
pageInfo: { hasNextPage: false },
nodes: [
{
login: 'a',
},
{
login: 'b',
},
{
login: 'c',
},
],
},
},
};
server.use(
graphqlMsw.query('orgs', (_req, res, ctx) => res(ctx.data(input))),
);
await expect(getOrganizationsFromUser(graphql, 'foo')).resolves.toEqual({
orgs: ['a', 'b', 'c'],
});
});
});
describe('getTeamMembers', () => {
it('reads team members', async () => {
const input: QueryResponse = {
@@ -33,6 +33,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node';
export type QueryResponse = {
organization?: OrganizationResponse;
repositoryOwner?: RepositoryOwnerResponse;
user?: UserResponse;
};
type RepositoryOwnerResponse = {
@@ -46,11 +47,19 @@ export type OrganizationResponse = {
repositories?: Connection<RepositoryResponse>;
};
export type UserResponse = {
organizations?: Connection<GithubOrg>;
};
export type PageInfo = {
hasNextPage: boolean;
endCursor?: string;
};
export type GithubOrg = {
login: string;
};
/**
* Github User
*
@@ -334,6 +343,34 @@ export async function getOrganizationTeamsFromUsers(
return { groups };
}
export async function getOrganizationsFromUser(
client: typeof graphql,
user: string,
): Promise<{
orgs: string[];
}> {
const query = `
query orgs($user: String!) {
user(login: $user) {
organizations(first: 100) {
nodes { login }
pageInfo { hasNextPage, endCursor }
}
}
}`;
const orgs = await queryWithPaging(
client,
query,
'',
r => r.user?.organizations,
async o => o.login,
{ user },
);
return { orgs };
}
export async function getOrganizationTeam(
client: typeof graphql,
org: string,
@@ -21,7 +21,9 @@ import {
DEFAULT_NAMESPACE,
Entity,
GroupEntity,
parseEntityRef,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
@@ -32,10 +34,24 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import {
DeferredEntity,
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { EventBroker, EventParams } from '@backstage/plugin-events-node';
import { graphql } from '@octokit/graphql';
import {
InstallationCreatedEvent,
InstallationEvent,
OrganizationEvent,
OrganizationMemberAddedEvent,
OrganizationMemberRemovedEvent,
MembershipEvent,
TeamCreatedEvent,
TeamDeletedEvent,
TeamEditedEvent,
TeamEvent,
} from '@octokit/webhooks-types';
import { merge } from 'lodash';
import * as uuid from 'uuid';
import { Logger } from 'winston';
@@ -44,6 +60,7 @@ import {
assignGroupsToUsers,
buildOrgHierarchy,
defaultOrganizationTeamTransformer,
defaultUserTransformer,
getOrganizationTeams,
getOrganizationUsers,
GithubTeam,
@@ -55,6 +72,11 @@ import {
ANNOTATION_GITHUB_TEAM_SLUG,
ANNOTATION_GITHUB_USER_LOGIN,
} from '../lib/annotation';
import {
getOrganizationsFromUser,
getOrganizationTeam,
getOrganizationTeamsFromUsers,
} from '../lib/github';
import { splitTeamSlug } from '../lib/util';
/**
@@ -118,8 +140,18 @@ export interface GithubMultiOrgEntityProviderOptions {
* By default groups will be namespaced according to their GitHub org.
*/
teamTransformer?: TeamTransformer;
/**
* An EventBroker to subscribe this provider to GitHub events to trigger delta mutations
*/
eventBroker?: EventBroker;
}
type CreateDeltaOperation = (entities: Entity[]) => {
added: DeferredEntity[];
removed: DeferredEntity[];
};
/**
* Ingests org data (users and groups) from GitHub.
*
@@ -161,6 +193,13 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
provider.schedule(options.schedule);
if (options.eventBroker) {
options.eventBroker.subscribe({
supportsEventTopics: provider.supportsEventTopics.bind(provider),
onEvent: provider.onEvent.bind(provider),
});
}
return provider;
}
@@ -271,6 +310,476 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
markCommitComplete();
}
private supportsEventTopics(): string[] {
return [
'github.installation',
'github.organization',
'github.team',
'github.membership',
];
}
private async onEvent(params: EventParams): Promise<void> {
const { logger } = this.options;
logger.debug(`Received event from ${params.topic}`);
const orgs = this.options.orgs?.length
? this.options.orgs
: await this.getAllOrgs(this.options.gitHubConfig);
const eventPayload = params.eventPayload as
| InstallationEvent
| OrganizationEvent
| MembershipEvent
| TeamEvent;
if (
!orgs.includes(
(eventPayload as InstallationEvent).installation?.account?.login,
) &&
!orgs.includes(
(eventPayload as OrganizationEvent | MembershipEvent | TeamEvent)
.organization?.login,
)
) {
return;
}
// https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#installation
if (
params.topic.includes('installation') &&
eventPayload.action === 'created'
) {
// We can only respond to installation.created events to add new users/groups since a
// installation.deleted event won't provide us info on what user/groups we should remove and
// we can't query the uninstalled org since we will no longer have access. This will need to be
// eventually resolved via occasional full mutation runs by calling read()
await this.onInstallationChange(
eventPayload as InstallationCreatedEvent,
orgs,
);
}
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization
if (
params.topic.includes('organization') &&
(eventPayload.action === 'member_added' ||
eventPayload.action === 'member_removed')
) {
await this.onMemberChangeInOrganization(eventPayload, orgs);
}
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#team
if (params.topic.includes('team')) {
if (
eventPayload.action === 'created' ||
eventPayload.action === 'deleted'
) {
await this.onTeamChangeInOrganization(
eventPayload as TeamCreatedEvent | TeamDeletedEvent,
);
} else if (eventPayload.action === 'edited') {
await this.onTeamEditedInOrganization(
eventPayload as TeamEditedEvent,
orgs,
);
}
}
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
if (params.topic.includes('membership')) {
this.onMembershipChangedInTeam(eventPayload as MembershipEvent, orgs);
}
return;
}
private async onInstallationChange(
event: InstallationCreatedEvent,
applicableOrgs: string[],
) {
if (!this.connection) {
throw new Error('Not initialized');
}
const org = event.installation.account.login;
const { headers, type: tokenType } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${org}`,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const { users } = await getOrganizationUsers(
client,
org,
tokenType,
this.options.userTransformer,
);
const { groups } = await getOrganizationTeams(
client,
org,
this.defaultMultiOrgTeamTransformer.bind(this),
);
// Fetch group memberships of users in case they already exist and
// have memberships in groups from other applicable orgs
for (const userOrg of applicableOrgs) {
const { headers: orgHeaders } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${userOrg}`,
});
const orgClient = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers: orgHeaders,
});
const { groups: userGroups } = await getOrganizationTeamsFromUsers(
orgClient,
userOrg,
users.map(
u =>
u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
u.metadata.name,
),
this.defaultMultiOrgTeamTransformer.bind(this),
);
assignGroupsToUsers(users, userGroups);
}
const { added, removed } = this.createAddEntitiesOperation([
...users,
...groups,
]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onMemberChangeInOrganization(
event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent,
applicableOrgs: string[],
) {
if (!this.connection) {
throw new Error('Not initialized');
}
const userTransformer =
this.options.userTransformer || defaultUserTransformer;
const { name, avatar_url: avatarUrl, email, login } = event.membership.user;
const org = event.organization.login;
const { headers } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${org}`,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const { orgs } = await getOrganizationsFromUser(client, login);
const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
let updateMemberships: boolean;
let createDeltaOperation: CreateDeltaOperation;
if (event.action === 'member_removed') {
if (userApplicableOrgs.length) {
// If the user is still associated with another applicable org then we don't want to remove
// them, just update the entity to remove any potential group memberships from the old org
createDeltaOperation = this.createAddEntitiesOperation.bind(this);
updateMemberships = true;
} else {
// User is no longer part of any applicable orgs so we can remove it,
// no need to take memberships into account
createDeltaOperation = this.createRemoveEntitiesOperation.bind(this);
updateMemberships = false;
}
} else {
// We're not sure if this user was already added as part of another applicable org
// so grab the latest memberships (potentially from teams of other orgs) to ensure
// we're not accidentally omitting them
createDeltaOperation = this.createAddEntitiesOperation.bind(this);
updateMemberships = true;
}
const user = (await userTransformer(
{
name,
avatarUrl,
login,
email: email ?? undefined,
},
{
org,
client,
query: '',
},
)) as UserEntity;
if (updateMemberships) {
for (const userOrg of userApplicableOrgs) {
const { headers: orgHeaders } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${userOrg}`,
});
const orgClient = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers: orgHeaders,
});
const { groups } = await getOrganizationTeamsFromUsers(
orgClient,
userOrg,
[login],
this.defaultMultiOrgTeamTransformer.bind(this),
);
assignGroupsToUsers([user], groups);
}
}
const { added, removed } = createDeltaOperation([user]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onTeamChangeInOrganization(
event: TeamCreatedEvent | TeamDeletedEvent,
) {
if (!this.connection) {
throw new Error('Not initialized');
}
const org = event.organization.login;
const { headers } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${org}`,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const { name, html_url: url, description, slug } = event.team;
const group = (await this.defaultMultiOrgTeamTransformer(
{
name,
slug,
editTeamUrl: `${url}/edit`,
combinedSlug: `${org}/${slug}`,
description: description ?? undefined,
parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
// entity will be removed or is new
members: [],
},
{
org,
client,
query: '',
},
)) as Entity;
const createDeltaOperation =
event.action === 'created'
? this.createAddEntitiesOperation.bind(this)
: this.createRemoveEntitiesOperation.bind(this);
const { added, removed } = createDeltaOperation([group]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onTeamEditedInOrganization(
event: TeamEditedEvent,
applicableOrgs: string[],
) {
if (!this.connection) {
throw new Error('Not initialized');
}
const org = event.organization.login;
const { headers, type: tokenType } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${org}`,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const teamSlug = event.team.slug;
const { group } = await getOrganizationTeam(
client,
org,
teamSlug,
this.defaultMultiOrgTeamTransformer.bind(this),
);
const { users } = await getOrganizationUsers(
client,
org,
tokenType,
this.options.userTransformer,
);
const usersFromChangedGroup =
group.spec.members?.map(m =>
stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
) || [];
const usersToRebuild = users.filter(u =>
usersFromChangedGroup.includes(stringifyEntityRef(u)),
);
// Update memberships of associated members of this group in case the group entity ref changed
for (const userOrg of applicableOrgs) {
const { headers: orgHeaders } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${userOrg}`,
});
const orgClient = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers: orgHeaders,
});
const { groups } = await getOrganizationTeamsFromUsers(
orgClient,
userOrg,
usersToRebuild.map(
u =>
u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
u.metadata.name,
),
this.defaultMultiOrgTeamTransformer.bind(this),
);
assignGroupsToUsers(usersToRebuild, groups);
}
const oldName = event.changes.name?.from || '';
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
const oldGroup = (await this.defaultMultiOrgTeamTransformer(
{
name: event.changes.name?.from,
slug: oldSlug,
combinedSlug: `${org}/${oldSlug}`,
description: event.changes.description?.from,
parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
// entity will be removed
members: [],
},
{
org,
client,
query: '',
},
)) as Entity;
// Remove the old group entity in case the entity ref is now different
const { removed } = this.createRemoveEntitiesOperation([oldGroup]);
const { added } = this.createAddEntitiesOperation([
...usersToRebuild,
group,
]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onMembershipChangedInTeam(
event: MembershipEvent,
applicableOrgs: string[],
) {
if (!this.connection) {
throw new Error('Not initialized');
}
// The docs are saying I will receive the slug for the removed event,
// but the types don't reflect that,
// so I will just check to be sure the slug is there
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
if (!('slug' in event.team)) {
return;
}
const org = event.organization.login;
const { headers } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${org}`,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const teamSlug = event.team.slug;
const { group } = await getOrganizationTeam(
client,
org,
teamSlug,
this.defaultMultiOrgTeamTransformer.bind(this),
);
const userTransformer =
this.options.userTransformer || defaultUserTransformer;
const { name, avatar_url: avatarUrl, email, login } = event.member;
const user = (await userTransformer(
{
name,
avatarUrl,
login,
email: email ?? undefined,
},
{
org,
client,
query: '',
},
)) as UserEntity;
const { orgs } = await getOrganizationsFromUser(client, login);
const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
for (const userOrg of userApplicableOrgs) {
const { headers: orgHeaders } =
await this.options.githubCredentialsProvider.getCredentials({
url: `${this.options.githubUrl}/${userOrg}`,
});
const orgClient = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers: orgHeaders,
});
const { groups } = await getOrganizationTeamsFromUsers(
orgClient,
userOrg,
[login],
this.defaultMultiOrgTeamTransformer.bind(this),
);
assignGroupsToUsers([user], groups);
}
const { added, removed } = this.createAddEntitiesOperation([user, group]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private schedule(schedule: GithubMultiOrgEntityProviderOptions['schedule']) {
if (!schedule || schedule === 'manual') {
return;
@@ -301,9 +810,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
team: GithubTeam,
ctx: TransformerContext,
): Promise<GroupEntity | undefined> {
const result = this.options.teamTransformer
? await this.options.teamTransformer(team, ctx)
: await defaultOrganizationTeamTransformer(team, ctx);
if (this.options.teamTransformer) {
return await this.options.teamTransformer(team, ctx);
}
const result = await defaultOrganizationTeamTransformer(team, ctx);
if (result) {
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
@@ -333,6 +844,32 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
)
.filter(Boolean) as string[];
}
private createAddEntitiesOperation(entities: Entity[]) {
return {
removed: [],
added: entities.map(entity => ({
locationKey: `github-multi-org-provider:${this.options.id}`,
entity: withLocations(
`https://${this.options.gitHubConfig.host}`,
entity,
),
})),
};
}
private createRemoveEntitiesOperation(entities: Entity[]) {
return {
added: [],
removed: entities.map(entity => ({
locationKey: `github-multi-org-provider:${this.options.id}`,
entity: withLocations(
`https://${this.options.gitHubConfig.host}`,
entity,
),
})),
};
}
}
// Helps wrap the timing and logging behaviors
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend-module-incremental-ingestion
## 0.3.2-next.2
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog-backend@1.9.1-next.2
- @backstage/config@1.0.7
## 0.3.2-next.1
### Patch Changes

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