Merge branch 'master' into canon-merge-field-input
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Explicitly rejects branch names containing a slash character.
|
||||
|
||||
The module now rejects any configuration that contains slashes in branch names. The reason for this is that the ingestion will run into downstream problems if they were let through. If you had configuration with a slash in the branch name in `filters.branch`, your application may fail to start up.
|
||||
|
||||
If you are affected by this, please move over to using branches that do not have slashes in them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-react': patch
|
||||
---
|
||||
|
||||
Fix catalog entity docs page not loading due to multiple addons data attachment in the New Frontend System.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
---
|
||||
|
||||
Updated the path field in the `gitlab:group:ensureExists` action to support also strings with multiple segments (e.g. `group/subgroup`)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
Added New Frontend System filter for the Kubernetes tab to use `isKubernetesAvailable` to control its visibility
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/canon': patch
|
||||
---
|
||||
|
||||
Internal refactor and fixes to the prop extraction logic for layout components.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Fixed the messaging in the `AlertDisplay` where it was claiming that there were older messages available rather than newer.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Expose the `UnpackNestedValue` type as it's been removed from `react-hook-form`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-slack': minor
|
||||
---
|
||||
|
||||
Added a new Slack NotificationProcessor for use with the notifications plugin
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
adding translation for `Register an existing component` text
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-server': minor
|
||||
'@backstage/plugin-events-backend-module-bitbucket-server': minor
|
||||
---
|
||||
|
||||
Added the ability for the plugin to receive events coming from Bitbucket Server push webhooks. It then performs a delta mutation on the catalog.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend': patch
|
||||
---
|
||||
|
||||
Allow throttling notification sending not to block the system with huge amount of receiving users
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fix the default catalog table search behavior by using the catalog table toolbar for all table variations (including when pagination mode is none).
|
||||
@@ -26,17 +26,30 @@ dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to you
|
||||
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server
|
||||
```
|
||||
|
||||
And update your backend by adding the following line:
|
||||
### Installation with New Backend System
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
/* highlight-add-start */
|
||||
```ts
|
||||
// optional if you want HTTP endpojnts to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend'));
|
||||
// optional if you want to use AWS SQS instead of HTTP endpoints to receive external events
|
||||
// backend.add(import('@backstage/plugin-events-backend-module-aws-sqs'));
|
||||
backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-bitbucket-server'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
You need to decide how you want to receive events from external sources like
|
||||
|
||||
- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md)
|
||||
- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md)
|
||||
|
||||
Further documentation:
|
||||
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md>
|
||||
- <https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-server/README.md>
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the entity provider, you'll need a [Bitbucket Server integration set up](locations.md).
|
||||
|
||||
@@ -152,7 +152,7 @@ If you do so, `default` will be used as provider ID.
|
||||
Wildcards cannot be used if the `validateLocationsExist` option is set to `true`.
|
||||
- **`filters`** _(optional)_:
|
||||
- **`branch`** _(optional)_:
|
||||
String used to filter results based on the branch name.
|
||||
String used to filter results based on the branch name. Branch name cannot have any slash (`/`) characters.
|
||||
Defaults to the default Branch of the repository.
|
||||
- **`repository`** _(optional)_:
|
||||
Regular expression used to filter results based on the repository name.
|
||||
|
||||
@@ -104,4 +104,69 @@ Apart from STMP, the email processor also supports the following transmissions:
|
||||
- sendmail
|
||||
- stream (only for debugging purposes)
|
||||
|
||||
See more information at https://github.com/backstage/backstage/blob/master/plugins/notifications-backend-module-email/README.md
|
||||
See more information at <https://github.com/backstage/backstage/blob/master/plugins/notifications-backend-module-email/README.md>
|
||||
|
||||
### Slack Processor
|
||||
|
||||
Slack processor is used to send notifications to users and channels in Slack.
|
||||
|
||||
### Slack Configuration
|
||||
|
||||
To use this you'll need to create a Slack App or use an existing one. It should have at least the following scopes:
|
||||
`chat:write`, `users:read`, `im:write` (for direct message support).
|
||||
|
||||
Additionally you may include scopes `chat:write.public` in order to send messages to public channels your app is not
|
||||
a member of.
|
||||
|
||||
These scopes are under OAuth & Permissions. You will also want to save the Bot User OAuth Token. This will be needed
|
||||
in the following step to configure `app-config.yaml`.
|
||||
|
||||
### Configure Backstage
|
||||
|
||||
To install the Slack processor, add the `@backstage/plugin-notifications-backend-module-slack` package to your backend.
|
||||
|
||||
```bash
|
||||
yarn workspace backend add @backstage/plugin-notifications-backend-module-slack
|
||||
```
|
||||
|
||||
Add the Slack processor to your backend:
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
import { createBackend } from '@backstage/plugin-notifications-backend';
|
||||
const backend = createBackend();
|
||||
// ...
|
||||
backend.add(import('@backstage/plugin-notifications-backend-module-slack'));
|
||||
```
|
||||
|
||||
Using the token you obtained from your Slack App, configure the Slack module in your `app-config.yaml`.
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
processors:
|
||||
slack:
|
||||
- token: xoxb-XXXXXXXXX
|
||||
broadcastChannels: # Optional, if you wish to support broadcast notifications.
|
||||
- C12345678
|
||||
```
|
||||
|
||||
Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send
|
||||
messages to more than one Slack workspace. Org-Wide App installation is not currently supported.
|
||||
|
||||
### Entity Requirements
|
||||
|
||||
Entities must be annotated with the following annotation:
|
||||
|
||||
- `slack.com/bot-notify`
|
||||
|
||||
The value may be any Slack ID supported by [chat.postMessage](https://api.slack.com/methods/chat.postMessage), for example a user (U12345678), channel (C12345678), group, or direct message chat.
|
||||
|
||||
It's also possible to use a user's email address or channel name, however IDs are recommended by Slack.
|
||||
Private channels/chats must use an ID.
|
||||
|
||||
### Observability
|
||||
|
||||
The processor includes the following counter metrics if you are exporting metrics using OpenTelemetry:
|
||||
|
||||
- `notifications.processors.slack.sent.count` - The number of messages sent
|
||||
- `notifications.processors.slack.error.count` - The number of messages that failed to send
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Renovate Hoster
|
||||
author: secustor
|
||||
authorUrl: https://github.com/secustor
|
||||
category: Quality
|
||||
description: This plugin enables you to host Renovate CLI yourself inside of Backstage and extract data from it.
|
||||
documentation: https://github.com/secustor/backstage-plugins/tree/main/plugins/renovate
|
||||
npmPackageName: '@backstage/backstage-plugin-renovate'
|
||||
addedDate: '2025-03-31'
|
||||
@@ -14,6 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
type BasePropDef = {
|
||||
type: string;
|
||||
values?: readonly unknown[];
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
responsive?: true;
|
||||
customProperties?: string[];
|
||||
};
|
||||
|
||||
export function extractProps(
|
||||
props: {
|
||||
className?: string;
|
||||
@@ -22,45 +32,53 @@ export function extractProps(
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
[key: string]: any;
|
||||
},
|
||||
propDefs: { [key: string]: any },
|
||||
propDefs: { [name in string]: BasePropDef },
|
||||
) {
|
||||
let className: string[] = (props.className || '').split(' ');
|
||||
let style: React.CSSProperties = { ...props.style };
|
||||
const hasProp = (key: string) => props.hasOwnProperty(key);
|
||||
|
||||
for (const key in propDefs) {
|
||||
const propDef = propDefs[key];
|
||||
|
||||
// Check if the prop is present or has a default value
|
||||
if (!hasProp(key) && !propDefs[key].hasOwnProperty('default')) {
|
||||
if (!Object.hasOwn(props, key) && !propDef.hasOwnProperty('default')) {
|
||||
continue; // Skip processing if neither is present
|
||||
}
|
||||
|
||||
const value = hasProp(key) ? props[key] : propDefs[key].default;
|
||||
const propDefsValues = propDefs[key].values;
|
||||
const propDefsCustomProperties = propDefs[key].customProperties;
|
||||
const propDefsClassName = propDefs[key].className;
|
||||
const isResponsive = propDefs[key].responsive;
|
||||
const value = Object.hasOwn(props, key)
|
||||
? (props[key] as unknown)
|
||||
: propDefs[key].default;
|
||||
const propDefsValues = propDef.values;
|
||||
const propDefsCustomProperties = propDef.customProperties;
|
||||
const propDefsClassName = propDef.className;
|
||||
const isResponsive = propDef.responsive;
|
||||
|
||||
const handleValue = (val: string, prefix: string = '') => {
|
||||
const handleValue = (val: unknown, prefix: string = '') => {
|
||||
// Skip adding class name if the key is "as"
|
||||
if (key === 'as') return;
|
||||
|
||||
if (propDefsValues.includes(val)) {
|
||||
if (propDefsValues?.includes(val)) {
|
||||
className.push(`${prefix}${propDefsClassName}-${val}`);
|
||||
} else {
|
||||
const customPropertyKey =
|
||||
isResponsive && prefix
|
||||
? `${propDefsCustomProperties}-${prefix.slice(0, -1)}`
|
||||
: propDefsCustomProperties;
|
||||
(style as any)[customPropertyKey] = val;
|
||||
if (propDefsCustomProperties) {
|
||||
for (const customProperty of propDefsCustomProperties) {
|
||||
const customPropertyKey =
|
||||
isResponsive && prefix
|
||||
? `${customProperty}-${prefix.slice(0, -1)}`
|
||||
: customProperty;
|
||||
style[customPropertyKey as keyof typeof style] = val as any;
|
||||
}
|
||||
}
|
||||
className.push(`${prefix}${propDefsClassName}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isResponsive && typeof value === 'object') {
|
||||
if (isResponsive && typeof value === 'object' && value !== null) {
|
||||
const breakpointValues = value as { [key: string]: unknown };
|
||||
// Handle responsive object values
|
||||
for (const breakpoint in value) {
|
||||
for (const breakpoint in breakpointValues) {
|
||||
const prefix = breakpoint === 'initial' ? '' : `${breakpoint}:`;
|
||||
handleValue(value[breakpoint], prefix);
|
||||
handleValue(breakpointValues[breakpoint], prefix);
|
||||
}
|
||||
} else {
|
||||
handleValue(value);
|
||||
|
||||
@@ -55,8 +55,8 @@ export const coreComponentsTranslationRef: TranslationRef<
|
||||
readonly 'oauthRequestDialog.rejectAll': 'Reject All';
|
||||
readonly 'supportButton.title': 'Support';
|
||||
readonly 'supportButton.close': 'Close';
|
||||
readonly 'alertDisplay.message_one': '({{ count }} older message)';
|
||||
readonly 'alertDisplay.message_other': '({{ count }} older messages)';
|
||||
readonly 'alertDisplay.message_one': '({{ count }} newer message)';
|
||||
readonly 'alertDisplay.message_other': '({{ count }} newer messages)';
|
||||
readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity';
|
||||
readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out";
|
||||
readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.';
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('<AlertDisplay />', () => {
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('(2 older messages)')).toBeInTheDocument();
|
||||
expect(screen.getByText('(2 newer messages)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,19 +189,19 @@ describe('<AlertDisplay />', () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(2 older messages)')).toBeInTheDocument();
|
||||
expect(queryByText('(2 newer messages)')).toBeInTheDocument();
|
||||
|
||||
// Validate removing messages
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(queryByText('transient message two')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
@@ -231,12 +231,12 @@ describe('<AlertDisplay />', () => {
|
||||
// 1s in, message 1 still shown, message 2 added in background
|
||||
act(() => jest.advanceTimersByTime(1000));
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
// 2s in, message 2 now shown, message 3 added
|
||||
act(() => jest.advanceTimersByTime(1000));
|
||||
expect(queryByText('transient message two')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
// 3.5s in, message 3 now shown
|
||||
act(() => jest.advanceTimersByTime(1500));
|
||||
@@ -264,7 +264,7 @@ describe('<AlertDisplay />', () => {
|
||||
// 1s in, message 1 still shown, message 2 added in background
|
||||
act(() => jest.advanceTimersByTime(1000));
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
// manually remove message 1
|
||||
fireEvent.click(screen.getByTestId('error-button-close'));
|
||||
@@ -273,7 +273,7 @@ describe('<AlertDisplay />', () => {
|
||||
// 2s in, message 2 now shown, message 3 added
|
||||
act(() => jest.advanceTimersByTime(1000));
|
||||
expect(queryByText('transient message two')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
// 3s in, message 3 now shown
|
||||
act(() => jest.advanceTimersByTime(1500));
|
||||
@@ -330,19 +330,19 @@ describe('<AlertDisplay />', () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(queryByText('transient message one')).toBeInTheDocument();
|
||||
expect(queryByText('(2 older messages)')).toBeInTheDocument();
|
||||
expect(queryByText('(2 newer messages)')).toBeInTheDocument();
|
||||
|
||||
// Validate removing messages
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(queryByText('permanent message')).toBeInTheDocument();
|
||||
expect(queryByText('(1 older message)')).toBeInTheDocument();
|
||||
expect(queryByText('(1 newer message)')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('error-button-close'));
|
||||
expect(queryByText('transient message three')).toBeInTheDocument();
|
||||
|
||||
@@ -106,8 +106,8 @@ export const coreComponentsTranslationRef = createTranslationRef({
|
||||
},
|
||||
},
|
||||
alertDisplay: {
|
||||
message_one: '({{ count }} older message)',
|
||||
message_other: '({{ count }} older messages)',
|
||||
message_one: '({{ count }} newer message)',
|
||||
message_other: '({{ count }} newer messages)',
|
||||
},
|
||||
autoLogout: {
|
||||
stillTherePrompt: {
|
||||
|
||||
@@ -21,16 +21,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
@@ -51,17 +47,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"p-throttle": "^4.1.1",
|
||||
"uuid": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^",
|
||||
"luxon": "^3.0.0",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
## API Report File for "@backstage/plugin-catalog-backend-module-bitbucket-server"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _feature: BackendFeature;
|
||||
export default _feature;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -3,13 +3,16 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerIntegrationConfig } from '@backstage/integration';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
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 { LocationSpec } from '@backstage/plugin-catalog-node';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api';
|
||||
@@ -22,6 +25,11 @@ export class BitbucketServerClient {
|
||||
config: BitbucketServerIntegrationConfig;
|
||||
}): BitbucketServerClient;
|
||||
// (undocumented)
|
||||
getDefaultBranch(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
}): Promise<BitbucketServerDefaultBranch>;
|
||||
// (undocumented)
|
||||
getFile(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
@@ -47,6 +55,16 @@ export class BitbucketServerClient {
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BitbucketServerDefaultBranch = {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: string;
|
||||
latestCommit: string;
|
||||
latestChangeset: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
@@ -55,9 +73,12 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
events?: EventsService;
|
||||
parser?: BitbucketServerLocationParser;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
catalogApi?: CatalogService;
|
||||
auth?: AuthService;
|
||||
},
|
||||
): BitbucketServerEntityProvider[];
|
||||
getProviderName(): string;
|
||||
@@ -108,6 +129,7 @@ export type BitbucketServerRepository = {
|
||||
}[]
|
||||
>;
|
||||
archived: boolean;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
*/
|
||||
|
||||
export { default } from './module';
|
||||
|
||||
export { BitbucketServerClient } from './lib';
|
||||
|
||||
export type {
|
||||
BitbucketServerProject,
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerPagedResponse,
|
||||
BitbucketServerListOptions,
|
||||
BitbucketServerDefaultBranch,
|
||||
} from './lib';
|
||||
|
||||
export { BitbucketServerEntityProvider } from './providers';
|
||||
export type { BitbucketServerLocationParser } from './providers';
|
||||
|
||||
+86
-1
@@ -23,7 +23,12 @@ import {
|
||||
BitbucketServerPagedResponse,
|
||||
paginated,
|
||||
} from './BitbucketServerClient';
|
||||
import { BitbucketServerProject, BitbucketServerRepository } from './types';
|
||||
import {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerProject,
|
||||
BitbucketServerRepository,
|
||||
} from './types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
@@ -122,6 +127,7 @@ describe('BitbucketServerClient', () => {
|
||||
],
|
||||
},
|
||||
archived: false,
|
||||
defaultBranch: 'master',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -200,6 +206,7 @@ describe('BitbucketServerClient', () => {
|
||||
],
|
||||
},
|
||||
archived: false,
|
||||
defaultBranch: 'master',
|
||||
};
|
||||
|
||||
return res(ctx.json(response));
|
||||
@@ -217,4 +224,82 @@ describe('BitbucketServerClient', () => {
|
||||
'https://bitbucket.mycompany.com/projects/test-project',
|
||||
);
|
||||
});
|
||||
|
||||
it('getRepository no repo', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/wrong-repo`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const error = async () => {
|
||||
await client.getRepository({
|
||||
projectKey: 'test-project',
|
||||
repo: 'wrong-repo',
|
||||
});
|
||||
};
|
||||
|
||||
await expect(error).rejects.toThrow(
|
||||
"Repository 'wrong-repo' in project 'test-project' does not exist.",
|
||||
);
|
||||
await expect(error).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('getDefaultBranch success', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/test-repo/default-branch`,
|
||||
(req, res, ctx) => {
|
||||
if (
|
||||
req.headers.get('authorization') !==
|
||||
'Basic dGVzdC11c2VyOnRlc3QtcHc='
|
||||
) {
|
||||
return res(ctx.status(400));
|
||||
}
|
||||
const response: BitbucketServerDefaultBranch = {
|
||||
id: 'refs/heads/master',
|
||||
displayId: 'master',
|
||||
type: 'BRANCH',
|
||||
latestCommit: '0f2f3ae484054696568bf4560ba4da280f7df82a',
|
||||
latestChangeset: '0f2f3ae484054696568bf4560ba4da280f7df82a',
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
return res(ctx.json(response));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const repo = await client.getDefaultBranch({
|
||||
projectKey: 'test-project',
|
||||
repo: 'test-repo',
|
||||
});
|
||||
expect(repo.displayId).toEqual('master');
|
||||
});
|
||||
|
||||
it('getDefaultBranch endpoint', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${config.apiBaseUrl}/projects/test-project/repos/wrong-repo/default-branch`,
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const error = async () => {
|
||||
await client.getDefaultBranch({
|
||||
projectKey: 'test-project',
|
||||
repo: 'wrong-repo',
|
||||
});
|
||||
};
|
||||
|
||||
await expect(error).rejects.toThrow(
|
||||
"Your Bitbucket Server version no longer supports the default branch endpoint or 'wrong-repo' in 'test-project' does not exist.",
|
||||
);
|
||||
await expect(error).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,14 @@ import {
|
||||
BitbucketServerIntegrationConfig,
|
||||
getBitbucketServerRequestOptions,
|
||||
} from '@backstage/integration';
|
||||
import { BitbucketServerProject, BitbucketServerRepository } from './types';
|
||||
import pThrottle from 'p-throttle';
|
||||
import {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerRepository,
|
||||
} from './index';
|
||||
import { BitbucketServerProject } from './types';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
// 1 per second
|
||||
const throttle = pThrottle({
|
||||
@@ -92,7 +98,35 @@ export class BitbucketServerClient {
|
||||
request,
|
||||
getBitbucketServerRequestOptions(this.config),
|
||||
);
|
||||
return response.json();
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(
|
||||
`Repository '${options.repo}' in project '${options.projectKey}' does not exist.`,
|
||||
);
|
||||
}
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
async getDefaultBranch(options: {
|
||||
projectKey: string;
|
||||
repo: string;
|
||||
}): Promise<BitbucketServerDefaultBranch> {
|
||||
const request = `${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/default-branch`;
|
||||
const response = await fetch(
|
||||
request,
|
||||
getBitbucketServerRequestOptions(this.config),
|
||||
);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(
|
||||
`Your Bitbucket Server version no longer supports the default branch endpoint or '${options.repo}' in '${options.projectKey}' does not exist.`,
|
||||
);
|
||||
}
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
resolvePath(options: { projectKey: string; repo: string; path: string }): {
|
||||
|
||||
@@ -20,6 +20,9 @@ export type {
|
||||
BitbucketServerListOptions,
|
||||
} from './BitbucketServerClient';
|
||||
export type {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerRepository,
|
||||
BitbucketServerEvents,
|
||||
BitbucketServerProject,
|
||||
} from './types';
|
||||
// export * from './types';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type BitbucketServerRepository = {
|
||||
type BitbucketServerRepository = {
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
@@ -28,9 +28,55 @@ export type BitbucketServerRepository = {
|
||||
}[]
|
||||
>;
|
||||
archived: boolean;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type BitbucketServerProject = {
|
||||
type BitbucketServerProject = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
type BitbucketServerDefaultBranch = {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: string;
|
||||
latestCommit: string;
|
||||
latestChangeset: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
namespace BitbucketServerEvents {
|
||||
interface Event {
|
||||
eventKey: string;
|
||||
}
|
||||
|
||||
export interface RefsChangedEvent extends Event {
|
||||
date: string;
|
||||
actor: Actor;
|
||||
repository: Repository;
|
||||
changes: Change[];
|
||||
commits: undefined;
|
||||
ToCommit: undefined;
|
||||
}
|
||||
type Actor = {
|
||||
name?: string;
|
||||
id: number;
|
||||
};
|
||||
type Change = {
|
||||
ref: { id: string; displayId: string; type: string };
|
||||
};
|
||||
type Repository = {
|
||||
slug: string;
|
||||
id: number;
|
||||
name: string;
|
||||
project: BitbucketServerProject;
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
BitbucketServerDefaultBranch,
|
||||
BitbucketServerProject,
|
||||
BitbucketServerEvents,
|
||||
BitbucketServerRepository,
|
||||
};
|
||||
|
||||
+49
-29
@@ -16,20 +16,33 @@
|
||||
|
||||
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider';
|
||||
import { BitbucketServerEntityProvider } from '../providers';
|
||||
import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider';
|
||||
|
||||
describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
it('should register provider at the catalog extension point', async () => {
|
||||
const events = new TestEventsService();
|
||||
const eventsServiceFactory = createServiceFactory({
|
||||
service: eventsServiceRef,
|
||||
deps: {},
|
||||
async factory({}) {
|
||||
return events;
|
||||
},
|
||||
});
|
||||
let addedProviders: Array<BitbucketServerEntityProvider> | undefined;
|
||||
let usedSchedule: SchedulerServiceTaskScheduleDefinition | undefined;
|
||||
|
||||
const extensionPoint = {
|
||||
const catalogExtensionPointImpl = {
|
||||
addEntityProvider: (providers: any) => {
|
||||
addedProviders = providers;
|
||||
},
|
||||
};
|
||||
const connection = jest.fn() as unknown as EntityProviderConnection;
|
||||
const runner = jest.fn();
|
||||
const scheduler = mockServices.scheduler.mock({
|
||||
createScheduledTaskRunner(schedule) {
|
||||
@@ -38,33 +51,35 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const config = {
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: 'bitbucket.mycompany.com',
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
await startTestBackend({
|
||||
extensionPoints: [
|
||||
[catalogProcessingExtensionPoint, catalogExtensionPointImpl],
|
||||
],
|
||||
features: [
|
||||
eventsServiceFactory,
|
||||
catalogModuleBitbucketServerEntityProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: 'bitbucket.mycompany.com',
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: 'bitbucket.mycompany.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]],
|
||||
features: [
|
||||
catalogModuleBitbucketServerEntityProvider,
|
||||
mockServices.rootConfig.factory({ data: config }),
|
||||
mockServices.logger.factory(),
|
||||
}),
|
||||
scheduler.factory,
|
||||
],
|
||||
});
|
||||
@@ -72,9 +87,14 @@ describe('catalogModuleBitbucketServerEntityProvider', () => {
|
||||
expect(usedSchedule?.frequency).toEqual({ months: 1 });
|
||||
expect(usedSchedule?.timeout).toEqual({ minutes: 3 });
|
||||
expect(addedProviders?.length).toEqual(1);
|
||||
expect(addedProviders?.pop()?.getProviderName()).toEqual(
|
||||
expect(runner).not.toHaveBeenCalled();
|
||||
const provider = addedProviders!.pop()!;
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'bitbucketServer-provider:default',
|
||||
);
|
||||
expect(runner).not.toHaveBeenCalled();
|
||||
await provider.connect(connection);
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('bitbucketServer-provider:default');
|
||||
expect(runner).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-2
@@ -18,8 +18,10 @@ import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
|
||||
import { BitbucketServerEntityProvider } from '../providers';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { BitbucketServerEntityProvider } from '../providers/BitbucketServerEntityProvider';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -31,14 +33,28 @@ export const catalogModuleBitbucketServerEntityProvider = createBackendModule({
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogProcessingExtensionPoint,
|
||||
catalogApi: catalogServiceRef,
|
||||
config: coreServices.rootConfig,
|
||||
events: eventsServiceRef,
|
||||
logger: coreServices.logger,
|
||||
scheduler: coreServices.scheduler,
|
||||
auth: coreServices.auth,
|
||||
},
|
||||
async init({ catalog, config, logger, scheduler }) {
|
||||
async init({
|
||||
catalog,
|
||||
catalogApi,
|
||||
config,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
auth,
|
||||
}) {
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi,
|
||||
events,
|
||||
logger,
|
||||
scheduler,
|
||||
auth,
|
||||
});
|
||||
|
||||
catalog.addEntityProvider(providers);
|
||||
|
||||
+478
-6
@@ -18,17 +18,30 @@ import {
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
SchedulerServiceTaskInvocationDefinition,
|
||||
BackstageCredentials,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
CatalogService,
|
||||
DeferredEntity,
|
||||
EntityProviderConnection,
|
||||
locationSpecToLocationEntity,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { BitbucketServerEntityProvider } from './BitbucketServerEntityProvider';
|
||||
import {
|
||||
BitbucketServerEntityProvider,
|
||||
toDeferredEntities,
|
||||
} from './BitbucketServerEntityProvider';
|
||||
import { BitbucketServerPagedResponse } from '../lib';
|
||||
import { Entity, LocationEntity } from '@backstage/catalog-model';
|
||||
import { BitbucketServerEvents } from '../lib/index';
|
||||
import { DefaultEventsService } from '@backstage/plugin-events-node';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
|
||||
class PersistingTaskRunner implements SchedulerServiceTaskRunner {
|
||||
private tasks: SchedulerServiceTaskInvocationDefinition[] = [];
|
||||
@@ -57,9 +70,17 @@ function pagedResponse(values: any): BitbucketServerPagedResponse<any> {
|
||||
|
||||
const logger = mockServices.logger.mock();
|
||||
|
||||
const authService = mockServices.auth.mock();
|
||||
|
||||
const events = DefaultEventsService.create({ logger });
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
function setupStubs(projects: Project[], baseUrl: string) {
|
||||
function setupStubs(
|
||||
projects: Project[],
|
||||
baseUrl: string,
|
||||
defaultBranch: string,
|
||||
) {
|
||||
// Stub projects
|
||||
server.use(
|
||||
rest.get(`${baseUrl}/rest/api/1.0/projects`, (_, res, ctx) => {
|
||||
@@ -93,6 +114,7 @@ function setupStubs(projects: Project[], baseUrl: string) {
|
||||
],
|
||||
},
|
||||
archived: repo.archived ?? false,
|
||||
defaultBranch: defaultBranch,
|
||||
});
|
||||
}
|
||||
return res(ctx.json(pagedResponse(response)));
|
||||
@@ -102,6 +124,102 @@ function setupStubs(projects: Project[], baseUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const targetPath = `/catalog-info.yaml`;
|
||||
const test1RepoUrl = `https://${host}/projects/TEST/repos/test1/browse`;
|
||||
|
||||
function setupRepositoryReqHandler(defaultBranch: string) {
|
||||
server.use(
|
||||
rest.get(
|
||||
`https://${host}/rest/api/1.0/projects/TEST/repos/test1`,
|
||||
(_, res, ctx) => {
|
||||
const response = {
|
||||
slug: 'test1',
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
project: {
|
||||
key: 'TEST',
|
||||
id: 1,
|
||||
name: 'TEST',
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: `https://${host}/projects/TEST`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
links: {
|
||||
self: [
|
||||
{
|
||||
href: `${test1RepoUrl}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultBranch: defaultBranch,
|
||||
};
|
||||
return res(ctx.json(response));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const repoPushEvent: BitbucketServerEvents.RefsChangedEvent = {
|
||||
eventKey: 'repo:refs_changed',
|
||||
date: '2017-09-19T09:45:32+1000',
|
||||
actor: {
|
||||
name: 'admin',
|
||||
id: 1,
|
||||
},
|
||||
repository: {
|
||||
slug: 'test1',
|
||||
id: 84,
|
||||
name: 'test1',
|
||||
project: {
|
||||
key: 'TEST',
|
||||
},
|
||||
},
|
||||
changes: [
|
||||
{
|
||||
ref: {
|
||||
id: 'refs/heads/master',
|
||||
displayId: 'master',
|
||||
type: 'BRANCH',
|
||||
},
|
||||
},
|
||||
],
|
||||
commits: undefined,
|
||||
ToCommit: undefined,
|
||||
};
|
||||
const repoPushEventParams = {
|
||||
topic: 'bitbucketServer.repo:refs_changed',
|
||||
eventPayload: repoPushEvent,
|
||||
metadata: { 'x-event-key': 'repo:refs_changed' },
|
||||
};
|
||||
|
||||
const createLocationEntity = (
|
||||
repoUrl: string,
|
||||
path: string,
|
||||
defaultBranch: string,
|
||||
): LocationEntity => {
|
||||
const target = `${repoUrl}${path}`;
|
||||
|
||||
const entity = locationSpecToLocationEntity({
|
||||
location: {
|
||||
type: 'url',
|
||||
target: target,
|
||||
presence: 'optional',
|
||||
},
|
||||
});
|
||||
entity.metadata.annotations = {
|
||||
...entity.metadata.annotations,
|
||||
[`${host}/repo-url`]: target,
|
||||
['bitbucket.org/default-branch']: defaultBranch,
|
||||
};
|
||||
|
||||
return entity;
|
||||
};
|
||||
|
||||
describe('BitbucketServerEntityProvider', () => {
|
||||
registerMswTestHooks(server);
|
||||
afterEach(() => {
|
||||
@@ -157,6 +275,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -191,6 +310,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
@@ -204,7 +324,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update on scheduled execution with filters', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -253,6 +372,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -270,6 +390,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:${url}`,
|
||||
'backstage.io/managed-by-origin-location': `url:${url}`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -291,7 +412,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update on scheduled execution without filters', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -329,6 +449,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -345,6 +466,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -364,6 +486,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
@@ -441,7 +564,6 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
});
|
||||
|
||||
it('apply full update with schedule in config', async () => {
|
||||
const host = 'bitbucket.mycompany.com';
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
@@ -488,6 +610,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
{ key: 'other-project', repos: [{ name: 'other-repo' }] },
|
||||
],
|
||||
`https://${host}`,
|
||||
'master',
|
||||
);
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
@@ -504,6 +627,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
@@ -523,6 +647,7 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
@@ -542,4 +667,351 @@ describe('BitbucketServerEntityProvider', () => {
|
||||
entities: expectedEntities,
|
||||
});
|
||||
});
|
||||
|
||||
it('Multiple location entities to deferred entities', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
host: host,
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const providers = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
events,
|
||||
schedule,
|
||||
});
|
||||
|
||||
expect(providers).toHaveLength(1);
|
||||
expect(providers[0].getProviderName()).toEqual(
|
||||
'bitbucketServer-provider:default',
|
||||
);
|
||||
|
||||
const locationEntities = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
[`${host}/repo-url`]: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-77f4323822420990f8c3e3c981d38c2dec4ae3a6',
|
||||
},
|
||||
spec: {
|
||||
presence: 'optional',
|
||||
target: `https://${host}/projects/project-test/repos/repo-test/browse/catalog-info.yaml`,
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'backstage.io/managed-by-origin-location': `url:https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
[`${host}/repo-url`]: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
'bitbucket.org/default-branch': 'master',
|
||||
},
|
||||
name: 'generated-d8d4944c30c2906dfee172ddda9537f9893b2c0f',
|
||||
},
|
||||
spec: {
|
||||
presence: 'optional',
|
||||
target: `https://${host}/projects/other-project/repos/other-repo/browse/catalog-info.yaml`,
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const deferredEntities = toDeferredEntities(
|
||||
locationEntities,
|
||||
providers[0].getProviderName(),
|
||||
);
|
||||
|
||||
expect(deferredEntities).toEqual([
|
||||
{
|
||||
locationKey: providers[0].getProviderName(),
|
||||
entity: locationEntities[0],
|
||||
},
|
||||
{
|
||||
locationKey: providers[0].getProviderName(),
|
||||
entity: locationEntities[1],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('refresh onRepoPush', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const keptModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/kept-module:${targetPath}`,
|
||||
'master',
|
||||
);
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
setupRepositoryReqHandler('master');
|
||||
|
||||
// authService.getOwnServiceCredentials();
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/kept-module:/catalog-info.yaml`,
|
||||
defaultBranch: 'master',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock.mock({
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
credentials: { credentials: BackstageCredentials },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
credentials.credentials !==
|
||||
(await authService.getOwnServiceCredentials()) ||
|
||||
request.filter.kind !== 'Location' ||
|
||||
request.filter[`metadata.annotations.${host}/repo-url`] !==
|
||||
`${test1RepoUrl}/kept-module:${targetPath}`
|
||||
) {
|
||||
return { items: [] };
|
||||
}
|
||||
return {
|
||||
items: [keptModule],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledWith({
|
||||
keys: [`url:${test1RepoUrl}/kept-module:${targetPath}`],
|
||||
});
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('no refresh onRepoPush due to different default branch', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const keptModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/kept-module:${targetPath}`,
|
||||
'main',
|
||||
);
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
setupRepositoryReqHandler('main');
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/kept-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock.mock({
|
||||
getEntities: async (
|
||||
request: { filter: Record<string, string> },
|
||||
credentials: { credentials: BackstageCredentials },
|
||||
): Promise<{ items: Entity[] }> => {
|
||||
if (
|
||||
credentials.credentials !==
|
||||
(await authService.getOwnServiceCredentials()) ||
|
||||
request.filter.kind !== 'Location' ||
|
||||
request.filter[`metadata.annotations.${host}/repo-url`] !==
|
||||
`${test1RepoUrl}/kept-module:${targetPath}`
|
||||
) {
|
||||
return { items: [] };
|
||||
}
|
||||
return {
|
||||
items: [keptModule],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('add onRepoPush', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
setupRepositoryReqHandler('master');
|
||||
authService.getPluginRequestToken.mockResolvedValue({
|
||||
token: 'fake-token',
|
||||
});
|
||||
const addedModule = createLocationEntity(
|
||||
test1RepoUrl,
|
||||
`/added-module:${targetPath}`,
|
||||
'master',
|
||||
);
|
||||
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/added-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock({ entities: [] });
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
const addedEntities = [
|
||||
{
|
||||
entity: addedModule,
|
||||
locationKey: 'bitbucketServer-provider:mainProvider',
|
||||
},
|
||||
];
|
||||
const removedEntities: DeferredEntity[] = [];
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledWith({
|
||||
keys: [],
|
||||
});
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'delta',
|
||||
added: addedEntities,
|
||||
removed: removedEntities,
|
||||
});
|
||||
});
|
||||
|
||||
it('fail add onRepoPush from wrong default branch', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
setupRepositoryReqHandler('main');
|
||||
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
bitbucketServer: [
|
||||
{
|
||||
host: host,
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketServer: {
|
||||
mainProvider: {
|
||||
host: host,
|
||||
apiBaseUrl: `https://${host}/rest/api/1.0`,
|
||||
catalogPath: `/added-module:/catalog-info.yaml`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catalogApi = catalogServiceMock({ entities: [] });
|
||||
const provider = BitbucketServerEntityProvider.fromConfig(config, {
|
||||
catalogApi: catalogApi as any as CatalogService,
|
||||
logger,
|
||||
schedule,
|
||||
events,
|
||||
auth: authService,
|
||||
})[0];
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
await events.publish(repoPushEventParams);
|
||||
|
||||
expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
+280
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, LocationEntity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
DeferredEntity,
|
||||
CatalogService,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import * as uuid from 'uuid';
|
||||
import { BitbucketServerClient, paginated } from '../lib';
|
||||
@@ -36,10 +38,16 @@ import {
|
||||
defaultBitbucketServerLocationParser,
|
||||
} from './BitbucketServerLocationParser';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
LoggerService,
|
||||
SchedulerService,
|
||||
SchedulerServiceTaskRunner,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BitbucketServerEvents } from '../lib';
|
||||
import { EventsService } from '@backstage/plugin-events-node';
|
||||
|
||||
const TOPIC_REPO_REFS_CHANGED = 'bitbucketServer.repo:refs_changed';
|
||||
|
||||
/**
|
||||
* Discovers catalog files located in Bitbucket Server.
|
||||
@@ -56,14 +64,23 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
private readonly logger: LoggerService;
|
||||
private readonly scheduleFn: () => Promise<void>;
|
||||
private connection?: EntityProviderConnection;
|
||||
private readonly catalogApi?: CatalogService;
|
||||
private readonly events?: EventsService;
|
||||
private readonly auth?: AuthService;
|
||||
private eventConfigErrorThrown = false;
|
||||
private readonly targetAnnotation: string;
|
||||
private readonly defaultBranchAnnotation: string;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: LoggerService;
|
||||
events?: EventsService;
|
||||
parser?: BitbucketServerLocationParser;
|
||||
schedule?: SchedulerServiceTaskRunner;
|
||||
scheduler?: SchedulerService;
|
||||
catalogApi?: CatalogService;
|
||||
auth?: AuthService;
|
||||
},
|
||||
): BitbucketServerEntityProvider[] {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -98,6 +115,9 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
options.logger,
|
||||
taskRunner,
|
||||
options.parser,
|
||||
options.catalogApi,
|
||||
options.events,
|
||||
options.auth,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -108,6 +128,9 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
logger: LoggerService,
|
||||
taskRunner: SchedulerServiceTaskRunner,
|
||||
parser?: BitbucketServerLocationParser,
|
||||
catalogApi?: CatalogService,
|
||||
events?: EventsService,
|
||||
auth?: AuthService,
|
||||
) {
|
||||
this.integration = integration;
|
||||
this.config = config;
|
||||
@@ -116,6 +139,11 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
target: this.getProviderName(),
|
||||
});
|
||||
this.scheduleFn = this.createScheduleFn(taskRunner);
|
||||
this.catalogApi = catalogApi;
|
||||
this.auth = auth;
|
||||
this.targetAnnotation = `${this.config.host.split(':')[0]}/repo-url`;
|
||||
this.defaultBranchAnnotation = 'bitbucket.org/default-branch';
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
private createScheduleFn(
|
||||
@@ -154,6 +182,22 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
async connect(connection: EntityProviderConnection): Promise<void> {
|
||||
this.connection = connection;
|
||||
await this.scheduleFn();
|
||||
|
||||
if (this.events) {
|
||||
await this.events.subscribe({
|
||||
id: this.getProviderName(),
|
||||
topics: [TOPIC_REPO_REFS_CHANGED],
|
||||
onEvent: async params => {
|
||||
if (params.topic !== TOPIC_REPO_REFS_CHANGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onRepoPush(
|
||||
params.eventPayload as BitbucketServerEvents.RefsChangedEvent,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(logger: LoggerService) {
|
||||
@@ -218,10 +262,245 @@ export class BitbucketServerEntityProvider implements EntityProvider {
|
||||
presence: 'optional',
|
||||
},
|
||||
})) {
|
||||
if (entity.metadata.annotations === undefined) {
|
||||
entity.metadata.annotations = {};
|
||||
}
|
||||
if (repository.defaultBranch === undefined) {
|
||||
const defaultBranchResponse = await client.getDefaultBranch({
|
||||
repo: repository.slug,
|
||||
projectKey: project.key,
|
||||
});
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
defaultBranchResponse.displayId;
|
||||
} else {
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
repository.defaultBranch;
|
||||
}
|
||||
result.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private isDefaultBranchPush(
|
||||
defaultBranch: String,
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): boolean {
|
||||
return event.changes.some(c => defaultBranch === c.ref.displayId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provider is able to handle events
|
||||
* @returns Boolean
|
||||
*/
|
||||
private canHandleEvents(): boolean {
|
||||
if (this.catalogApi && this.auth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.eventConfigErrorThrown) {
|
||||
this.eventConfigErrorThrown = true;
|
||||
throw new Error(
|
||||
`${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or AuthService.`,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getLocationEntity(
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): Promise<Entity[]> {
|
||||
const client = BitbucketServerClient.fromConfig({
|
||||
config: this.integration.config,
|
||||
});
|
||||
const result: Entity[] = [];
|
||||
try {
|
||||
const repository = await client.getRepository({
|
||||
projectKey: event.repository.project.key,
|
||||
repo: event.repository.slug,
|
||||
});
|
||||
|
||||
for await (const entity of this.parser({
|
||||
client,
|
||||
logger: this.logger,
|
||||
location: {
|
||||
type: 'url',
|
||||
target: `${repository.links.self[0].href}${this.config.catalogPath}`,
|
||||
presence: 'optional',
|
||||
},
|
||||
})) {
|
||||
entity.metadata.annotations![
|
||||
this.targetAnnotation
|
||||
] = `${repository.links.self[0].href}${this.config.catalogPath}`;
|
||||
|
||||
if (entity.metadata.annotations === undefined) {
|
||||
entity.metadata.annotations = {};
|
||||
}
|
||||
|
||||
if (repository.defaultBranch === undefined) {
|
||||
const defaultBranchResponse = await client.getDefaultBranch({
|
||||
repo: repository.slug,
|
||||
projectKey: event.repository.project.key,
|
||||
});
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
defaultBranchResponse.displayId;
|
||||
} else {
|
||||
entity.metadata.annotations[this.defaultBranchAnnotation] =
|
||||
repository.defaultBranch;
|
||||
}
|
||||
result.push(entity);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'NotFoundError') {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async onRepoPush(
|
||||
event: BitbucketServerEvents.RefsChangedEvent,
|
||||
): Promise<void> {
|
||||
if (!this.canHandleEvents()) {
|
||||
this.logger.error(
|
||||
'Bitbucket Server catalog entity provider is not set up to handle events. Missing authService or catalogApi.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
const repoSlug = event.repository.slug;
|
||||
const catalogRepoUrl: string = `https://${this.config.host}/projects/${event.repository.project.key}/repos/${repoSlug}/browse${this.config.catalogPath}`;
|
||||
this.logger.info(`handle repo:push event for ${catalogRepoUrl}`);
|
||||
const targets = await this.getLocationEntity(event);
|
||||
if (targets.length === 0) {
|
||||
this.logger.error('Failed to create location entity.');
|
||||
return;
|
||||
}
|
||||
const existing = await this.findExistingLocations(
|
||||
catalogRepoUrl,
|
||||
await this.auth!.getOwnServiceCredentials(),
|
||||
);
|
||||
const stillExisting: LocationEntity[] = [];
|
||||
const removed: DeferredEntity[] = [];
|
||||
existing.forEach(item => {
|
||||
if (
|
||||
targets.find(
|
||||
value =>
|
||||
value.metadata.annotations![this.targetAnnotation] ===
|
||||
item.spec.target,
|
||||
)
|
||||
) {
|
||||
stillExisting.push(item);
|
||||
} else {
|
||||
removed.push({
|
||||
locationKey: this.getProviderName(),
|
||||
entity: item,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const added = await this.getAddedEntities(targets, existing);
|
||||
|
||||
if (
|
||||
stillExisting.length > 0 &&
|
||||
stillExisting[0].metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
stillExisting[0].metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
added.length > 0 &&
|
||||
added[0].entity.metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
added[0].entity.metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
removed.length > 0 &&
|
||||
removed[0].entity.metadata.annotations![this.defaultBranchAnnotation] !==
|
||||
undefined &&
|
||||
!this.isDefaultBranchPush(
|
||||
removed[0].entity.metadata.annotations![this.defaultBranchAnnotation],
|
||||
event,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [
|
||||
this.connection.refresh({
|
||||
keys: stillExisting.map(entity => `url:${entity.spec.target}`),
|
||||
}),
|
||||
];
|
||||
|
||||
if (added.length > 0 || removed.length > 0) {
|
||||
promises.push(
|
||||
this.connection.applyMutation({
|
||||
type: 'delta',
|
||||
added: added,
|
||||
removed: removed,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async getAddedEntities(
|
||||
targets: Entity[],
|
||||
existing: LocationEntity[],
|
||||
): Promise<DeferredEntity[]> {
|
||||
const added: DeferredEntity[] = toDeferredEntities(
|
||||
targets.filter(
|
||||
target =>
|
||||
!existing.find(
|
||||
item =>
|
||||
item.spec.target ===
|
||||
target.metadata.annotations![this.targetAnnotation],
|
||||
),
|
||||
),
|
||||
this.getProviderName(),
|
||||
);
|
||||
return added;
|
||||
}
|
||||
|
||||
private async findExistingLocations(
|
||||
catalogRepoUrl: string,
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<LocationEntity[]> {
|
||||
const filter: Record<string, string> = {};
|
||||
filter.kind = 'Location';
|
||||
filter[`metadata.annotations.${this.targetAnnotation}`] = catalogRepoUrl;
|
||||
|
||||
return this.catalogApi!.getEntities({ filter }, { credentials }).then(
|
||||
result => result.items,
|
||||
) as Promise<LocationEntity[]>;
|
||||
}
|
||||
}
|
||||
|
||||
export function toDeferredEntities(
|
||||
targets: Entity[],
|
||||
locationKey: string,
|
||||
): DeferredEntity[] {
|
||||
return targets.map(entity => {
|
||||
return {
|
||||
locationKey,
|
||||
entity,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
-2
@@ -48,10 +48,8 @@ export function readProviderConfigs(
|
||||
// simple/single config variant
|
||||
return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)];
|
||||
}
|
||||
|
||||
return providersConfig.keys().map(id => {
|
||||
const providerConfig = providersConfig.getConfig(id);
|
||||
|
||||
return readProviderConfig(id, providerConfig);
|
||||
});
|
||||
}
|
||||
|
||||
+3
-5
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LocationSpec,
|
||||
locationSpecToLocationEntity,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import { locationSpecToLocationEntity } from '@backstage/plugin-catalog-node';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { BitbucketServerClient } from '../lib';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-common';
|
||||
import { BitbucketServerClient } from '../lib';
|
||||
|
||||
/**
|
||||
* A custom callback that reacts to finding a location by yielding entities.
|
||||
|
||||
+20
@@ -311,4 +311,24 @@ describe('readProviderConfigs', () => {
|
||||
|
||||
expect(() => readProviderConfigs(config)).toThrow();
|
||||
});
|
||||
|
||||
it('throws an error when filters.branch contains a slash', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
github: {
|
||||
invalidBranchUser: {
|
||||
organization: 'test-org',
|
||||
catalogPath: '/*/catalog-info.yaml',
|
||||
filters: {
|
||||
branch: 'test/a',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => readProviderConfigs(config)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,12 @@ function readProviderConfig(
|
||||
);
|
||||
}
|
||||
|
||||
if (branchPattern?.includes('/')) {
|
||||
throw new Error(
|
||||
'Error while processing GitHub provider config. Slash characters (/) are not allowed in filters.branch',
|
||||
);
|
||||
}
|
||||
|
||||
const schedule = config.has('schedule')
|
||||
? readSchedulerServiceTaskScheduleDefinitionFromConfig(
|
||||
config.getConfig('schedule'),
|
||||
|
||||
@@ -10,6 +10,16 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const catalogImportTranslationRef: TranslationRef<
|
||||
'catalog-import',
|
||||
{
|
||||
readonly pageTitle: 'Register an existing component';
|
||||
readonly 'importInfoCard.title': 'Register an existing component';
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: FrontendPlugin<
|
||||
|
||||
@@ -15,13 +15,13 @@ import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { FieldErrors } from 'react-hook-form';
|
||||
import { InfoCardVariants } from '@backstage/core-components';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { NestedValue } from 'react-hook-form';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { ScmAuthApi } from '@backstage/integration-react';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { SubmitHandler } from 'react-hook-form';
|
||||
import { TextFieldProps } from '@material-ui/core/TextField/TextField';
|
||||
import { UnpackNestedValue } from 'react-hook-form';
|
||||
import { UseFormProps } from 'react-hook-form';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
@@ -361,6 +361,17 @@ export interface StepPrepareCreatePullRequestProps {
|
||||
) => React_2.ReactNode;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export type UnpackNestedValue<T> = T extends NestedValue<infer U>
|
||||
? U
|
||||
: T extends Date | FileList | File | Blob
|
||||
? T
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]: UnpackNestedValue<T[K]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/api/CatalogImportApi.d.ts:25:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts
|
||||
|
||||
@@ -38,6 +38,8 @@ import { CatalogImportClient, catalogImportApiRef } from './api';
|
||||
import { rootRouteRef } from './plugin';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
export * from './translation';
|
||||
|
||||
// TODO: It's currently possible to override the import page with a custom one. We need to decide
|
||||
// whether this type of override is typically done with an input or by overriding the entire extension.
|
||||
const catalogImportPage = PageBlueprint.make({
|
||||
|
||||
@@ -28,6 +28,8 @@ import { useTheme } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { ImportInfoCard } from '../ImportInfoCard';
|
||||
import { ImportStepper } from '../ImportStepper';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogImportTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* The default catalog import page.
|
||||
@@ -35,6 +37,7 @@ import { ImportStepper } from '../ImportStepper';
|
||||
* @public
|
||||
*/
|
||||
export const DefaultImportPage = () => {
|
||||
const { t } = useTranslationRef(catalogImportTranslationRef);
|
||||
const theme = useTheme();
|
||||
const configApi = useApi(configApiRef);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@@ -54,7 +57,7 @@ export const DefaultImportPage = () => {
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Register an existing component" />
|
||||
<Header title={t('pageTitle')} />
|
||||
<Content>
|
||||
<ContentHeader title={`Start tracking your component in ${appTitle}`}>
|
||||
<SupportButton>{supportTitle}</SupportButton>
|
||||
|
||||
@@ -25,6 +25,8 @@ import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CatalogImportApi, catalogImportApiRef } from '../../api';
|
||||
import { ImportInfoCard } from './ImportInfoCard';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogImportTranslationRef } from '../../translation';
|
||||
|
||||
describe('<ImportInfoCard />', () => {
|
||||
let apis: TestApiRegistry;
|
||||
@@ -50,6 +52,14 @@ describe('<ImportInfoCard />', () => {
|
||||
});
|
||||
|
||||
it('renders without exploding', async () => {
|
||||
let translatedText = '';
|
||||
|
||||
const TestComponent = () => {
|
||||
const { t } = useTranslationRef(catalogImportTranslationRef);
|
||||
translatedText = t('importInfoCard.title');
|
||||
return <ImportInfoCard />;
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
@@ -57,13 +67,10 @@ describe('<ImportInfoCard />', () => {
|
||||
[catalogImportApiRef, catalogImportApi],
|
||||
]}
|
||||
>
|
||||
<ImportInfoCard />
|
||||
<TestComponent />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Register an existing component'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(translatedText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders section on GitHub discovery if supported', async () => {
|
||||
|
||||
@@ -21,6 +21,8 @@ import Typography from '@material-ui/core/Typography';
|
||||
import React from 'react';
|
||||
import { catalogImportApiRef } from '../../api';
|
||||
import { useCatalogFilename } from '../../hooks';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogImportTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* Props for {@link ImportInfoCard}.
|
||||
@@ -43,6 +45,7 @@ export const ImportInfoCard = (props: ImportInfoCardProps) => {
|
||||
exampleRepositoryUrl = 'https://github.com/backstage/backstage',
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslationRef(catalogImportTranslationRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const appTitle = configApi.getOptionalString('app.title') || 'Backstage';
|
||||
const catalogImportApi = useApi(catalogImportApiRef);
|
||||
@@ -53,7 +56,7 @@ export const ImportInfoCard = (props: ImportInfoCardProps) => {
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title="Register an existing component"
|
||||
title={t('importInfoCard.title')}
|
||||
titleTypographyProps={{ component: 'h3' }}
|
||||
deepLink={{
|
||||
title: 'Learn more about the Software Catalog',
|
||||
|
||||
+17
-1
@@ -27,7 +27,7 @@ import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { UnpackNestedValue, UseFormReturn } from 'react-hook-form';
|
||||
import { NestedValue, UseFormReturn } from 'react-hook-form';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import YAML from 'yaml';
|
||||
import { AnalyzeResult, catalogImportApiRef } from '../../api';
|
||||
@@ -56,6 +56,22 @@ type FormData = {
|
||||
useCodeowners: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper for unpacking NestedValue into the underlying type.
|
||||
*
|
||||
* @public
|
||||
* @deprecated This is a copy of the type from react-hook-form, and will be removed in a future release
|
||||
*/
|
||||
export type UnpackNestedValue<T> = T extends NestedValue<infer U>
|
||||
? U
|
||||
: T extends Date | FileList | File | Blob
|
||||
? T
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]: UnpackNestedValue<T[K]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Props for {@link StepPrepareCreatePullRequest}.
|
||||
*
|
||||
|
||||
@@ -23,4 +23,7 @@ export type { PreviewCatalogInfoComponentProps } from './PreviewCatalogInfoCompo
|
||||
export { PreviewPullRequestComponent } from './PreviewPullRequestComponent';
|
||||
export type { PreviewPullRequestComponentProps } from './PreviewPullRequestComponent';
|
||||
export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest';
|
||||
export type { StepPrepareCreatePullRequestProps } from './StepPrepareCreatePullRequest';
|
||||
export type {
|
||||
StepPrepareCreatePullRequestProps,
|
||||
UnpackNestedValue,
|
||||
} from './StepPrepareCreatePullRequest';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const catalogImportTranslationRef = createTranslationRef({
|
||||
id: 'catalog-import',
|
||||
messages: {
|
||||
pageTitle: 'Register an existing component',
|
||||
importInfoCard: {
|
||||
title: 'Register an existing component',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -49,6 +49,7 @@ import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { catalogTranslationRef } from '../../alpha/translation';
|
||||
import { FavoriteToggleIcon } from '@backstage/core-components';
|
||||
import { CatalogTableToolbar } from './CatalogTableToolbar';
|
||||
|
||||
/**
|
||||
* Props for {@link CatalogTable}.
|
||||
@@ -251,6 +252,9 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
actions={actions}
|
||||
subtitle={subtitle}
|
||||
emptyContent={emptyContent}
|
||||
components={{
|
||||
Toolbar: CatalogTableToolbar,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,36 @@
|
||||
# events-backend-module-bitbucket-server
|
||||
|
||||
Welcome to the `events-backend-module-bitbucket-server` backend plugin!
|
||||
|
||||
This package is a module for the `events-backend` backend plugin
|
||||
and extends the event system with an `BitbucketServerEventRouter`.
|
||||
|
||||
The event router will subscribe to the topic `bitbucketServer`
|
||||
and route the events to more concrete topics based on the value
|
||||
of the provided `x-event-key` metadata field.
|
||||
|
||||
Examples:
|
||||
|
||||
| x-event-key | topic |
|
||||
| ------------------- | ----------------------------------- |
|
||||
| `repo:refs_changed` | `bitbucketServer.repo:refs_changed` |
|
||||
| `repo:modified` | `bitbucketServer.repo:modified` |
|
||||
|
||||
Please find all possible webhook event types at the
|
||||
[official documentation](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the [`events-backend` plugin](../events-backend/README.md).
|
||||
|
||||
Install this module:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-server
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
backend.add(import('@backstage/plugin-events-backend-module-bitbucket-server'));
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-events-backend-module-bitbucket-server
|
||||
title: '@backstage/plugin-events-backend-module-bitbucket-server'
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@backstage/plugin-events-backend-module-bitbucket-server",
|
||||
"version": "0.0.0",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "events",
|
||||
"pluginPackage": "@backstage/plugin-events-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/events-backend-module-bitbucket-server"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
## API Report File for "@backstage/plugin-events-backend-module-bitbucket-server"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
const eventsModuleBitbucketServerEventRouter: BackendFeature;
|
||||
export default eventsModuleBitbucketServerEventRouter;
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The module "bitbucket-server" for the Backstage backend plugin "events-backend"
|
||||
* adding an event router for Bitbucket Server.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export { eventsModuleBitbucketServerEventRouter as default } from './service/eventsModuleBitbucketServerEventRouter';
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { BitbucketServerEventRouter } from './BitbucketServerEventRouter';
|
||||
|
||||
describe('BitbucketServerEventRouter', () => {
|
||||
const events = new TestEventsService();
|
||||
const eventRouter = new BitbucketServerEventRouter({ events });
|
||||
const topic = 'bitbucketServer';
|
||||
const eventPayload = { test: 'payload' };
|
||||
const metadata = { 'x-event-key': 'test:type' };
|
||||
|
||||
beforeEach(() => {
|
||||
events.reset();
|
||||
});
|
||||
|
||||
it('subscribed to topic', () => {
|
||||
eventRouter.subscribe();
|
||||
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter');
|
||||
expect(events.subscribed[0].topics).toEqual([topic]);
|
||||
});
|
||||
|
||||
it('no x-event-key', () => {
|
||||
eventRouter.onEvent({ topic, eventPayload });
|
||||
|
||||
expect(events.published).toEqual([]);
|
||||
});
|
||||
|
||||
it('with x-event-key', () => {
|
||||
eventRouter.onEvent({ topic, eventPayload, metadata });
|
||||
|
||||
expect(events.published.length).toBe(1);
|
||||
expect(events.published[0].topic).toEqual('bitbucketServer.test:type');
|
||||
expect(events.published[0].eventPayload).toEqual(eventPayload);
|
||||
expect(events.published[0].metadata).toEqual(metadata);
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 {
|
||||
EventParams,
|
||||
EventsService,
|
||||
SubTopicEventRouter,
|
||||
} from '@backstage/plugin-events-node';
|
||||
|
||||
/**
|
||||
* Subscribes to the generic `bitbucketServer` topic
|
||||
* and publishes the events under the more concrete sub-topic
|
||||
* depending on the `x-event-key` provided.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class BitbucketServerEventRouter extends SubTopicEventRouter {
|
||||
constructor(options: { events: EventsService }) {
|
||||
super({
|
||||
events: options.events,
|
||||
topic: 'bitbucketServer',
|
||||
});
|
||||
}
|
||||
|
||||
protected getSubscriberId(): string {
|
||||
return 'BitbucketServerEventRouter';
|
||||
}
|
||||
|
||||
protected determineSubTopic(params: EventParams): string | undefined {
|
||||
return params.metadata?.['x-event-key'] as string | undefined;
|
||||
}
|
||||
}
|
||||
+44
@@ -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 { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { BitbucketServerEventRouter } from '../router/BitbucketServerEventRouter';
|
||||
|
||||
/**
|
||||
* Module for the events-backend plugin, adding an event router for Bitbucket Server.
|
||||
*
|
||||
* Registers the `BitbucketServerEventRouter`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const eventsModuleBitbucketServerEventRouter = createBackendModule({
|
||||
pluginId: 'events',
|
||||
moduleId: 'bitbucket-server-event-router',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
events: eventsServiceRef,
|
||||
},
|
||||
async init({ events }) {
|
||||
const eventRouter = new BitbucketServerEventRouter({
|
||||
events,
|
||||
});
|
||||
await eventRouter.subscribe();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { createServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { TestEventsService } from '@backstage/plugin-events-backend-test-utils';
|
||||
import { eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { eventsModuleBitbucketServerEventRouter } from './eventsModuleBitbucketServerEventRouter';
|
||||
|
||||
describe('eventsModuleBitbucketServerEventRouter', () => {
|
||||
it('should be correctly wired and set up', async () => {
|
||||
const events = new TestEventsService();
|
||||
const eventsServiceFactory = createServiceFactory({
|
||||
service: eventsServiceRef,
|
||||
deps: {},
|
||||
async factory({}) {
|
||||
return events;
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
features: [eventsServiceFactory, eventsModuleBitbucketServerEventRouter],
|
||||
});
|
||||
|
||||
expect(events.subscribed).toHaveLength(1);
|
||||
expect(events.subscribed[0].id).toEqual('BitbucketServerEventRouter');
|
||||
});
|
||||
});
|
||||
+2
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,8 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { default as feature } from './module';
|
||||
|
||||
/** @alpha */
|
||||
const _feature = feature;
|
||||
export default _feature;
|
||||
export {};
|
||||
@@ -17,6 +17,7 @@
|
||||
import React from 'react';
|
||||
import { compatWrapper } from '@backstage/core-compat-api';
|
||||
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { isKubernetesAvailable } from '../Router';
|
||||
|
||||
export const entityKubernetesContent = EntityContentBlueprint.make({
|
||||
name: 'kubernetes',
|
||||
@@ -24,7 +25,7 @@ export const entityKubernetesContent = EntityContentBlueprint.make({
|
||||
defaultPath: '/kubernetes',
|
||||
defaultTitle: 'Kubernetes',
|
||||
defaultGroup: 'deployment',
|
||||
filter: 'kind:component,resource',
|
||||
filter: isKubernetesAvailable,
|
||||
loader: () =>
|
||||
import('./KubernetesContentPage').then(m =>
|
||||
compatWrapper(<m.KubernetesContentPage />),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# @backstage/plugin-notifications-backend-module-slack
|
||||
|
||||
The Slack backend module for the notifications plugin.
|
||||
|
||||
See [Built-in Processors](https://backstage.io/docs/notifications/processors/#built-in-processors) for detailed documentation
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-notifications-backend-module-slack
|
||||
title: '@backstage/plugin-notifications-backend-module-slack'
|
||||
description: The slack backend module for the notifications plugin.
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 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 interface Config {
|
||||
notifications?: {
|
||||
processors?: {
|
||||
slack?: Array<{
|
||||
/**
|
||||
* Slack Bot Token. Usually starts with `xoxb-`.
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* Broadcast notification receivers when receiver is set to config
|
||||
* These can be Slack User IDs, Slack User Email addresses, Slack Channel
|
||||
* Names, or Slack Channel IDs. Any valid identifier that chat.postMessage can accept.
|
||||
*/
|
||||
broadcastChannels?: string[];
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@backstage/plugin-notifications-backend-module-slack",
|
||||
"version": "0.0.0",
|
||||
"description": "The slack backend module for the notifications plugin.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "notifications",
|
||||
"pluginPackage": "@backstage/plugin-notifications-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/notifications-backend-module-slack"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-notifications-common": "workspace:^",
|
||||
"@backstage/plugin-notifications-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@slack/bolt": "^3.21.4",
|
||||
"@slack/types": "^2.14.0",
|
||||
"@slack/web-api": "^7.5.0",
|
||||
"dataloader": "^2.0.0",
|
||||
"p-throttle": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"msw": "^2.0.0"
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
## API Report File for "@backstage/plugin-notifications-backend-module-slack"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify';
|
||||
|
||||
// @public
|
||||
const notificationsModuleSlack: BackendFeature;
|
||||
export default notificationsModuleSlack;
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The slack backend module for the notifications plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib';
|
||||
export { notificationsModuleSlack as default } from './module';
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* Copyright 2025 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 { mockServices } from '@backstage/backend-test-utils';
|
||||
import { SlackNotificationProcessor } from './SlackNotificationProcessor';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
jest.mock('@slack/web-api', () => {
|
||||
const mockSlack = {
|
||||
chat: {
|
||||
postMessage: jest.fn(() => ({
|
||||
ok: true,
|
||||
ts: '1234567890.123456',
|
||||
channel: 'C12345678',
|
||||
})),
|
||||
},
|
||||
conversations: {
|
||||
list: jest.fn(() => ({
|
||||
ok: true,
|
||||
channels: [{ id: 'C12345678', name: 'test' }],
|
||||
})),
|
||||
},
|
||||
users: {
|
||||
list: jest.fn(() => ({
|
||||
ok: true,
|
||||
members: [
|
||||
{
|
||||
id: 'U12345678',
|
||||
name: 'test',
|
||||
profile: { email: 'test@example.com' },
|
||||
real_name: 'Test User',
|
||||
is_bot: false,
|
||||
is_app_user: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
};
|
||||
return { WebClient: jest.fn(() => mockSlack) };
|
||||
});
|
||||
|
||||
const DEFAULT_ENTITIES_RESPONSE = {
|
||||
items: [
|
||||
{
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'mock',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'slack.com/bot-notify': 'U12345678',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'group:default/mock',
|
||||
},
|
||||
} as unknown as Entity,
|
||||
{
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
name: 'mock',
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
'slack.com/bot-notify': 'C12345678',
|
||||
},
|
||||
},
|
||||
} as unknown as Entity,
|
||||
],
|
||||
};
|
||||
|
||||
describe('SlackNotificationProcessor', () => {
|
||||
const logger = mockServices.logger.mock();
|
||||
const auth = mockServices.auth();
|
||||
const discovery = mockServices.discovery();
|
||||
const config = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should send a notification to a group', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'entity', entityRef: 'group:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith({
|
||||
channel: 'C12345678',
|
||||
text: 'notification',
|
||||
attachments: [
|
||||
{
|
||||
color: '#00A699',
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
accessory: {
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View More',
|
||||
},
|
||||
action_id: 'button-action',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Severity: normal',
|
||||
emoji: true,
|
||||
},
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Topic: N/A',
|
||||
emoji: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fallback: 'notification',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user notification is sent directly', () => {
|
||||
it('should send a notification to a user', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: '/catalog/user/default/jane.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith({
|
||||
channel: 'U12345678',
|
||||
text: 'notification',
|
||||
attachments: [
|
||||
{
|
||||
color: '#00A699',
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
accessory: {
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View More',
|
||||
},
|
||||
action_id: 'button-action',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Severity: normal',
|
||||
emoji: true,
|
||||
},
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Topic: N/A',
|
||||
emoji: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fallback: 'notification',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user notification is expanded from a group', () => {
|
||||
it('should not send a notification', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: '/catalog/user/default/jane.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'group:default/group' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when broadcast channels are not configured', () => {
|
||||
it('should not send broadcast messages', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: '/catalog/user/default/jane.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
expect(slack.chat.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when broadcast channels are configured', () => {
|
||||
it('should send broadcast messages', async () => {
|
||||
const slack = new WebClient();
|
||||
const broadcastConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
broadcastChannels: ['C12345678', 'D12345678'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(broadcastConfig, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: null,
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
link: '/catalog/user/default/jane.doe',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'broadcast' },
|
||||
payload: { title: 'notification' },
|
||||
},
|
||||
);
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright 2025 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 {
|
||||
AuthService,
|
||||
DiscoveryService,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Entity, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import {
|
||||
NotificationProcessor,
|
||||
NotificationSendOptions,
|
||||
} from '@backstage/plugin-notifications-node';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import { Counter, metrics } from '@opentelemetry/api';
|
||||
import { ChatPostMessageArguments, WebClient } from '@slack/web-api';
|
||||
import DataLoader from 'dataloader';
|
||||
import pThrottle from 'p-throttle';
|
||||
import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants';
|
||||
import { toChatPostMessageArgs } from './util';
|
||||
|
||||
export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
private readonly logger: LoggerService;
|
||||
private readonly catalog: CatalogApi;
|
||||
private readonly auth: AuthService;
|
||||
private readonly slack: WebClient;
|
||||
private readonly sendNotifications;
|
||||
private readonly messagesSent: Counter;
|
||||
private readonly messagesFailed: Counter;
|
||||
private readonly broadcastChannels?: string[];
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
auth: AuthService;
|
||||
discovery: DiscoveryService;
|
||||
logger: LoggerService;
|
||||
catalog: CatalogApi;
|
||||
slack?: WebClient;
|
||||
broadcastChannels?: string[];
|
||||
},
|
||||
): SlackNotificationProcessor[] {
|
||||
const slackConfig =
|
||||
config.getOptionalConfigArray('notifications.processors.slack') ?? [];
|
||||
return slackConfig.map(c => {
|
||||
const token = c.getString('token');
|
||||
const slack = options.slack ?? new WebClient(token);
|
||||
const broadcastChannels = c.getOptionalStringArray('broadcastChannels');
|
||||
return new SlackNotificationProcessor({
|
||||
slack,
|
||||
broadcastChannels,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(options: {
|
||||
slack: WebClient;
|
||||
auth: AuthService;
|
||||
discovery: DiscoveryService;
|
||||
logger: LoggerService;
|
||||
catalog: CatalogApi;
|
||||
broadcastChannels?: string[];
|
||||
}) {
|
||||
const { auth, catalog, logger, slack, broadcastChannels } = options;
|
||||
this.logger = logger;
|
||||
this.catalog = catalog;
|
||||
this.auth = auth;
|
||||
this.slack = slack;
|
||||
this.broadcastChannels = broadcastChannels;
|
||||
|
||||
const meter = metrics.getMeter('default');
|
||||
this.messagesSent = meter.createCounter(
|
||||
'notifications.processors.slack.sent.count',
|
||||
{
|
||||
description: 'Number of messages sent to Slack successfully',
|
||||
},
|
||||
);
|
||||
this.messagesFailed = meter.createCounter(
|
||||
'notifications.processors.slack.error.count',
|
||||
{
|
||||
description: 'Number of messages that failed to send to Slack',
|
||||
},
|
||||
);
|
||||
|
||||
const throttle = pThrottle({
|
||||
limit: 10,
|
||||
interval: durationToMilliseconds({ minutes: 1 }),
|
||||
});
|
||||
const throttled = throttle((opts: ChatPostMessageArguments) =>
|
||||
this.sendNotification(opts),
|
||||
);
|
||||
this.sendNotifications = async (opts: ChatPostMessageArguments[]) => {
|
||||
const results = await Promise.allSettled(
|
||||
opts.map(message => throttled(message)),
|
||||
);
|
||||
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
results.forEach(result => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successCount++;
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Failed to send Slack channel notification: ${result.reason.message}`,
|
||||
);
|
||||
failureCount++;
|
||||
}
|
||||
});
|
||||
|
||||
this.messagesSent.add(successCount);
|
||||
this.messagesFailed.add(failureCount);
|
||||
};
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'SlackNotificationProcessor';
|
||||
}
|
||||
|
||||
async processOptions(
|
||||
options: NotificationSendOptions,
|
||||
): Promise<NotificationSendOptions> {
|
||||
if (options.recipients.type !== 'entity') {
|
||||
return options;
|
||||
}
|
||||
|
||||
const entityRefs = [options.recipients.entityRef].flat();
|
||||
|
||||
const outbound: ChatPostMessageArguments[] = [];
|
||||
await Promise.all(
|
||||
entityRefs.map(async entityRef => {
|
||||
const compoundEntityRef = parseEntityRef(entityRef);
|
||||
// skip users as they are sent direct messages
|
||||
if (compoundEntityRef.kind === 'user') {
|
||||
return;
|
||||
}
|
||||
|
||||
let channel;
|
||||
try {
|
||||
channel = await this.getSlackNotificationTarget(entityRef);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to get Slack channel for entity: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
this.logger.debug(`No Slack channel found for entity: ${entityRef}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Sending notification with payload: ${JSON.stringify(
|
||||
options.payload,
|
||||
)}`,
|
||||
);
|
||||
|
||||
const payload = toChatPostMessageArgs({
|
||||
channel,
|
||||
payload: options.payload,
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`Sending Slack channel notification: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
outbound.push(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
console.log('dispatching message');
|
||||
await this.sendNotifications(outbound);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async postProcess(
|
||||
notification: Notification,
|
||||
options: NotificationSendOptions,
|
||||
): Promise<void> {
|
||||
const destinations: string[] = [];
|
||||
|
||||
// Handle broadcast case
|
||||
if (notification.user === null) {
|
||||
destinations.push(...(this.broadcastChannels ?? []));
|
||||
} else if (options.recipients.type === 'entity') {
|
||||
// Handle user-specific notification
|
||||
const entityRefs = [options.recipients.entityRef].flat();
|
||||
if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) {
|
||||
// We've already dispatched a slack channel message, so let's not send a DM.
|
||||
return;
|
||||
}
|
||||
|
||||
const destination = await this.getSlackNotificationTarget(
|
||||
notification.user,
|
||||
);
|
||||
|
||||
if (!destination) {
|
||||
this.logger.error(
|
||||
`No slack.com/bot-notify annotation found for user: ${notification.user}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
destinations.push(destination);
|
||||
}
|
||||
|
||||
// If no destinations, nothing to do
|
||||
if (destinations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare outbound messages
|
||||
const outbound = destinations.map(channel =>
|
||||
toChatPostMessageArgs({ channel, payload: options.payload }),
|
||||
);
|
||||
|
||||
// Log debug info
|
||||
outbound.forEach(payload => {
|
||||
this.logger.debug(`Sending notification: ${JSON.stringify(payload)}`);
|
||||
});
|
||||
|
||||
// Send notifications
|
||||
await this.sendNotifications(outbound);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
entityRefs: readonly string[],
|
||||
): Promise<(Entity | undefined)[]> {
|
||||
const { token } = await this.auth.getPluginRequestToken({
|
||||
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
|
||||
const response = await this.catalog.getEntitiesByRefs(
|
||||
{
|
||||
entityRefs: entityRefs.slice(),
|
||||
fields: [`metadata.annotations.${ANNOTATION_SLACK_BOT_NOTIFY}`],
|
||||
},
|
||||
{
|
||||
token,
|
||||
},
|
||||
);
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
async getSlackNotificationTarget(
|
||||
entityRef: string,
|
||||
): Promise<string | undefined> {
|
||||
const entityLoader = new DataLoader<string, Entity | undefined>(
|
||||
entityRefs => this.getEntities(entityRefs),
|
||||
);
|
||||
const entity = await entityLoader.load(entityRef);
|
||||
|
||||
if (!entity) {
|
||||
console.log(`Entity not found: ${entityRef}`);
|
||||
throw new NotFoundError(`Entity not found: ${entityRef}`);
|
||||
}
|
||||
|
||||
return entity?.metadata?.annotations?.[ANNOTATION_SLACK_BOT_NOTIFY];
|
||||
}
|
||||
|
||||
async sendNotification(args: ChatPostMessageArguments): Promise<void> {
|
||||
const response = await this.slack.chat.postMessage(args);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to send notification: ${response.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @public
|
||||
* The annotation key for the entity's Slack ID. This can be
|
||||
* any valid chat.postMessage destination including:
|
||||
* - A user ID (U12345678)
|
||||
* - A channel ID (C12345678)
|
||||
* - A DM ID (D12345678)
|
||||
* - A group ID (S12345678)
|
||||
*
|
||||
* It can also be a user's email address or a channel name,
|
||||
* however IDs are preferred.
|
||||
*/
|
||||
export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 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 { SlackNotificationProcessor } from './SlackNotificationProcessor';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 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 interface SlackNotificationOptions {
|
||||
url: string;
|
||||
payload: string;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2025 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 {
|
||||
NotificationPayload,
|
||||
NotificationSeverity,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api';
|
||||
|
||||
export function toChatPostMessageArgs(options: {
|
||||
channel: string;
|
||||
payload: NotificationPayload;
|
||||
}): ChatPostMessageArguments {
|
||||
const { channel, payload } = options;
|
||||
|
||||
const args: ChatPostMessageArguments = {
|
||||
channel,
|
||||
text: payload.title,
|
||||
attachments: [
|
||||
{
|
||||
color: getColor(payload.severity),
|
||||
blocks: toSlackBlockKit(payload),
|
||||
fallback: payload.title,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function toSlackBlockKit(payload: NotificationPayload): KnownBlock[] {
|
||||
const { description, link, severity, topic } = payload;
|
||||
return [
|
||||
{
|
||||
type: 'section',
|
||||
...(description && {
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: description ?? 'No description provided',
|
||||
},
|
||||
}),
|
||||
accessory: {
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View More',
|
||||
},
|
||||
...(link && { url: link }),
|
||||
action_id: 'button-action',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: `Severity: ${severity ?? 'normal'}`,
|
||||
emoji: true,
|
||||
},
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: `Topic: ${topic ?? 'N/A'}`,
|
||||
emoji: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getColor(severity: NotificationSeverity | undefined) {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return '#FF0000'; // Red
|
||||
case 'high':
|
||||
return '#FFA500'; // Orange
|
||||
case 'low':
|
||||
case 'normal':
|
||||
default:
|
||||
return '#00A699'; // Neutral color
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 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 {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node';
|
||||
import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor';
|
||||
|
||||
/**
|
||||
* The Slack notification processor for use with the notifications plugin.
|
||||
* This allows sending of notifications via Slack DMs or to channels.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const notificationsModuleSlack = createBackendModule({
|
||||
pluginId: 'notifications',
|
||||
moduleId: 'slack',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
config: coreServices.rootConfig,
|
||||
discovery: coreServices.discovery,
|
||||
logger: coreServices.logger,
|
||||
notifications: notificationsProcessingExtensionPoint,
|
||||
},
|
||||
async init({ auth, config, discovery, logger, notifications }) {
|
||||
const catalogClient = new CatalogClient({
|
||||
discoveryApi: discovery,
|
||||
});
|
||||
|
||||
notifications.addProcessor(
|
||||
SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogClient,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Configuration options for notifications-backend
|
||||
*/
|
||||
notifications?: {
|
||||
/**
|
||||
* Concurrency limit for notification sending, defaults to 10
|
||||
*/
|
||||
concurrencyLimit?: number;
|
||||
/**
|
||||
* Throttle duration between notification sending, defaults to 50ms
|
||||
*/
|
||||
throttleInterval?: HumanDuration | string;
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts",
|
||||
"migrations/**/*.{js,d.ts}"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -49,9 +50,11 @@
|
||||
"@backstage/plugin-notifications-common": "workspace:^",
|
||||
"@backstage/plugin-notifications-node": "workspace:^",
|
||||
"@backstage/plugin-signals-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"knex": "^3.0.0",
|
||||
"p-throttle": "^4.1.1",
|
||||
"uuid": "^11.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
@@ -68,5 +71,6 @@
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^1.0.0",
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ import {
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
|
||||
import { getUsersForEntityRef } from './getUsersForEntityRef';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Config, readDurationFromConfig } from '@backstage/config';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import pThrottle from 'p-throttle';
|
||||
|
||||
/** @internal */
|
||||
export interface RouterOptions {
|
||||
@@ -82,6 +84,19 @@ export async function createRouter(
|
||||
const WEB_NOTIFICATION_CHANNEL = 'Web';
|
||||
const store = await DatabaseNotificationsStore.create({ database });
|
||||
const frontendBaseUrl = config.getString('app.baseUrl');
|
||||
const concurrencyLimit =
|
||||
config.getOptionalNumber('notifications.concurrencyLimit') ?? 10;
|
||||
const throttleInterval = config.has('notifications.throttleInterval')
|
||||
? durationToMilliseconds(
|
||||
readDurationFromConfig(config, {
|
||||
key: 'notifications.throttleInterval',
|
||||
}),
|
||||
)
|
||||
: 50;
|
||||
const throttle = pThrottle({
|
||||
limit: concurrencyLimit,
|
||||
interval: throttleInterval,
|
||||
});
|
||||
|
||||
const getUser = async (req: Request<unknown>) => {
|
||||
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
|
||||
@@ -475,66 +490,79 @@ export async function createRouter(
|
||||
return notification;
|
||||
};
|
||||
|
||||
const sendUserNotification = async (
|
||||
baseNotification: Omit<Notification, 'user' | 'id'>,
|
||||
user: string,
|
||||
opts: NotificationSendOptions,
|
||||
origin: string,
|
||||
scope?: string,
|
||||
): Promise<Notification | undefined> => {
|
||||
const userNotification = {
|
||||
...baseNotification,
|
||||
id: uuid(),
|
||||
user,
|
||||
};
|
||||
const notification = await preProcessNotification(userNotification, opts);
|
||||
|
||||
const enabled = await isNotificationsEnabled({
|
||||
user,
|
||||
channel: WEB_NOTIFICATION_CHANNEL,
|
||||
origin: userNotification.origin,
|
||||
});
|
||||
|
||||
let ret = notification;
|
||||
|
||||
if (!enabled) {
|
||||
postProcessNotification(ret, opts);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let existingNotification;
|
||||
if (scope) {
|
||||
existingNotification = await store.getExistingScopeNotification({
|
||||
user,
|
||||
scope,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingNotification) {
|
||||
const restored = await store.restoreExistingNotification({
|
||||
id: existingNotification.id,
|
||||
notification,
|
||||
});
|
||||
ret = restored ?? notification;
|
||||
} else {
|
||||
await store.saveNotification(notification);
|
||||
}
|
||||
|
||||
if (signals) {
|
||||
await signals.publish<NewNotificationSignal>({
|
||||
recipients: { type: 'user', entityRef: [user] },
|
||||
message: {
|
||||
action: 'new_notification',
|
||||
notification_id: ret.id,
|
||||
},
|
||||
channel: 'notifications',
|
||||
});
|
||||
}
|
||||
postProcessNotification(ret, opts);
|
||||
return ret;
|
||||
};
|
||||
|
||||
const sendUserNotifications = async (
|
||||
baseNotification: Omit<Notification, 'user' | 'id'>,
|
||||
users: string[],
|
||||
opts: NotificationSendOptions,
|
||||
origin: string,
|
||||
) => {
|
||||
const notifications = [];
|
||||
): Promise<Notification[]> => {
|
||||
const { scope } = opts.payload;
|
||||
const uniqueUsers = [...new Set(users)];
|
||||
for (const user of uniqueUsers) {
|
||||
const userNotification = {
|
||||
...baseNotification,
|
||||
id: uuid(),
|
||||
user,
|
||||
};
|
||||
const notification = await preProcessNotification(userNotification, opts);
|
||||
|
||||
const enabled = await isNotificationsEnabled({
|
||||
user,
|
||||
channel: WEB_NOTIFICATION_CHANNEL,
|
||||
origin: userNotification.origin,
|
||||
});
|
||||
|
||||
let ret = notification;
|
||||
if (enabled) {
|
||||
let existingNotification;
|
||||
if (scope) {
|
||||
existingNotification = await store.getExistingScopeNotification({
|
||||
user,
|
||||
scope,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingNotification) {
|
||||
const restored = await store.restoreExistingNotification({
|
||||
id: existingNotification.id,
|
||||
notification,
|
||||
});
|
||||
ret = restored ?? notification;
|
||||
} else {
|
||||
await store.saveNotification(notification);
|
||||
}
|
||||
|
||||
notifications.push(ret);
|
||||
|
||||
if (signals) {
|
||||
await signals.publish<NewNotificationSignal>({
|
||||
recipients: { type: 'user', entityRef: [user] },
|
||||
message: {
|
||||
action: 'new_notification',
|
||||
notification_id: ret.id,
|
||||
},
|
||||
channel: 'notifications',
|
||||
});
|
||||
}
|
||||
}
|
||||
postProcessNotification(ret, opts);
|
||||
}
|
||||
return notifications;
|
||||
const throttled = throttle((user: string) =>
|
||||
sendUserNotification(baseNotification, user, opts, origin, scope),
|
||||
);
|
||||
const sent = await Promise.all(uniqueUsers.map(user => throttled(user)));
|
||||
return sent.filter(n => n !== undefined);
|
||||
};
|
||||
|
||||
const createNotificationHandler = async (
|
||||
|
||||
+51
@@ -243,4 +243,55 @@ describe('gitlab:group:ensureExists', () => {
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
|
||||
});
|
||||
|
||||
it(`Should ${examples[5].description}`, async () => {
|
||||
mockGitlabClient.Groups.search.mockResolvedValue([
|
||||
{
|
||||
id: 1,
|
||||
full_path: 'group1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_path: 'group1/group2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
full_path: 'group1/group2/group3',
|
||||
},
|
||||
]);
|
||||
mockGitlabClient.Groups.create.mockResolvedValue({
|
||||
id: 4,
|
||||
full_path: 'group1/group2/group3/group4',
|
||||
});
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token: 'tokenlols',
|
||||
apiBaseUrl: 'https://api.gitlab.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
const action = createGitlabGroupEnsureExistsAction({ integrations });
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: yaml.parse(examples[5].example).steps[0].input,
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith(
|
||||
'Group 4',
|
||||
'group4',
|
||||
{
|
||||
parentId: 3,
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('groupId', 4);
|
||||
});
|
||||
});
|
||||
|
||||
+21
@@ -102,4 +102,25 @@ export const examples: TemplateExample[] = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Create a group nested within another group using path and objects',
|
||||
example: yaml.stringify({
|
||||
steps: [
|
||||
{
|
||||
id: 'gitlabGroup',
|
||||
name: 'Group',
|
||||
action: 'gitlab:group:ensureExists',
|
||||
input: {
|
||||
repoUrl: 'gitlab.com',
|
||||
path: [
|
||||
'group1/group2',
|
||||
{ name: 'Group 3', slug: 'group3' },
|
||||
{ name: 'Group 4', slug: 'group4' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -94,6 +94,38 @@ describe('gitlab:group:ensureExists', () => {
|
||||
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
|
||||
});
|
||||
|
||||
it('should create a new group from pathstring if it does not exists', async () => {
|
||||
mockGitlabClient.Groups.search.mockResolvedValue([
|
||||
{
|
||||
id: 1,
|
||||
full_path: 'bar',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_path: 'foo',
|
||||
},
|
||||
]);
|
||||
|
||||
mockGitlabClient.Groups.create.mockResolvedValue({
|
||||
id: 3,
|
||||
full_path: 'foo/bar',
|
||||
});
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gitlab.com?repo=repo&owner=owner',
|
||||
path: ['foo/bar', 'baz'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith('bar', 'bar', {
|
||||
parentId: 2,
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith('groupId', 3);
|
||||
});
|
||||
|
||||
it('should create a new group from object if it does not exists', async () => {
|
||||
mockGitlabClient.Groups.search.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -76,11 +76,7 @@ export const createGitlabGroupEnsureExistsAction = (options: {
|
||||
|
||||
let currentPath: string | null = null;
|
||||
let parentId: number | null = null;
|
||||
for (const pathElement of path) {
|
||||
const slug =
|
||||
typeof pathElement === 'string' ? pathElement : pathElement.slug;
|
||||
const name =
|
||||
typeof pathElement === 'string' ? pathElement : pathElement.name;
|
||||
for (const { name, slug } of pathIterator(path)) {
|
||||
const fullPath: string = currentPath ? `${currentPath}/${slug}` : slug;
|
||||
const result = (await api.Groups.search(
|
||||
fullPath,
|
||||
@@ -119,3 +115,19 @@ export const createGitlabGroupEnsureExistsAction = (options: {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type PathPart = { name: string; slug: string };
|
||||
type PathItem = string | PathPart;
|
||||
|
||||
function* pathIterator(items: PathItem[]): Generator<PathPart> {
|
||||
for (const item of items) {
|
||||
if (typeof item === 'string') {
|
||||
const parts = item.split('/');
|
||||
for (const part of parts) {
|
||||
yield { name: part, slug: part };
|
||||
}
|
||||
} else {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { TechDocsAddonOptions } from './types';
|
||||
import { attachComponentData } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
attachComponentData,
|
||||
getComponentData,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { getDataKeyByName, TECHDOCS_ADDONS_KEY } from './addons';
|
||||
import {
|
||||
@@ -53,6 +57,19 @@ export const attachTechDocsAddonComponentData = <P>(
|
||||
techDocsAddon: ComponentType<P>,
|
||||
data: TechDocsAddonOptions,
|
||||
) => {
|
||||
attachComponentData(techDocsAddon, TECHDOCS_ADDONS_KEY, data);
|
||||
attachComponentData(techDocsAddon, getDataKeyByName(data.name), true);
|
||||
const element = React.createElement(techDocsAddon as ComponentType);
|
||||
|
||||
const isDataAttached = getComponentData<TechDocsAddonOptions>(
|
||||
element,
|
||||
TECHDOCS_ADDONS_KEY,
|
||||
);
|
||||
if (!isDataAttached) {
|
||||
attachComponentData(techDocsAddon, TECHDOCS_ADDONS_KEY, data);
|
||||
}
|
||||
|
||||
const dataKey = getDataKeyByName(data.name);
|
||||
const isDataKeyAttached = getComponentData<boolean>(element, dataKey);
|
||||
if (!isDataKeyAttached) {
|
||||
attachComponentData(techDocsAddon, dataKey, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5535,12 +5535,16 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/catalog-client": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^"
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
luxon: ^3.0.0
|
||||
msw: ^1.0.0
|
||||
p-throttle: ^4.1.1
|
||||
@@ -6355,6 +6359,18 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-events-backend-module-bitbucket-server@workspace:plugins/events-backend-module-bitbucket-server"
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/plugin-events-backend-test-utils": "workspace:^"
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit"
|
||||
@@ -6778,6 +6794,33 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-notifications-backend-module-slack@workspace:plugins/notifications-backend-module-slack":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-notifications-backend-module-slack@workspace:plugins/notifications-backend-module-slack"
|
||||
dependencies:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/catalog-client": "workspace:^"
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
"@backstage/plugin-notifications-common": "workspace:^"
|
||||
"@backstage/plugin-notifications-node": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@faker-js/faker": ^8.4.1
|
||||
"@opentelemetry/api": ^1.9.0
|
||||
"@slack/bolt": ^3.21.4
|
||||
"@slack/types": ^2.14.0
|
||||
"@slack/web-api": ^7.5.0
|
||||
dataloader: ^2.0.0
|
||||
msw: ^2.0.0
|
||||
p-throttle: ^4.1.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-notifications-backend@workspace:^, @backstage/plugin-notifications-backend@workspace:plugins/notifications-backend":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-notifications-backend@workspace:plugins/notifications-backend"
|
||||
@@ -6800,12 +6843,14 @@ __metadata:
|
||||
"@backstage/plugin-notifications-node": "workspace:^"
|
||||
"@backstage/plugin-signals-backend": "workspace:^"
|
||||
"@backstage/plugin-signals-node": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@types/express": ^4.17.6
|
||||
"@types/supertest": ^2.0.8
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
knex: ^3.0.0
|
||||
msw: ^1.0.0
|
||||
p-throttle: ^4.1.1
|
||||
supertest: ^7.0.0
|
||||
uuid: ^11.0.0
|
||||
winston: ^3.2.1
|
||||
@@ -9643,6 +9688,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@faker-js/faker@npm:^8.4.1":
|
||||
version: 8.4.1
|
||||
resolution: "@faker-js/faker@npm:8.4.1"
|
||||
checksum: d802d531f8929562715adc279cfec763c9a4bc596ec67b0ce43fd0ae61b285d2b0eec6f1f4aa852452a63721a842fe7e81926dce7bd92acca94b01e2a1f55f5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fastify/busboy@npm:^2.0.0":
|
||||
version: 2.1.1
|
||||
resolution: "@fastify/busboy@npm:2.1.1"
|
||||
@@ -16402,6 +16454,121 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/bolt@npm:^3.21.4":
|
||||
version: 3.22.0
|
||||
resolution: "@slack/bolt@npm:3.22.0"
|
||||
dependencies:
|
||||
"@slack/logger": ^4.0.0
|
||||
"@slack/oauth": ^2.6.3
|
||||
"@slack/socket-mode": ^1.3.6
|
||||
"@slack/types": ^2.13.0
|
||||
"@slack/web-api": ^6.13.0
|
||||
"@types/express": ^4.16.1
|
||||
"@types/promise.allsettled": ^1.0.3
|
||||
"@types/tsscmp": ^1.0.0
|
||||
axios: ^1.7.4
|
||||
express: ^4.21.0
|
||||
path-to-regexp: ^8.1.0
|
||||
promise.allsettled: ^1.0.2
|
||||
raw-body: ^2.3.3
|
||||
tsscmp: ^1.0.6
|
||||
checksum: edd5c7cf658808effde87c936f19a0cc2b7d49ac97471651f2b1bb3db0074b92dc8ad3c9657577105d93c48df9ba16c382902c0d90082854cbbe86bfc7753827
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/logger@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@slack/logger@npm:3.0.0"
|
||||
dependencies:
|
||||
"@types/node": ">=12.0.0"
|
||||
checksum: 6512d0e9e4be47ea465705ab9b6e6901f36fa981da0d4a657fde649d452b567b351002049b5ee0a22569b5119bf6c2f61befd5b8022d878addb7a99c91b03389
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/logger@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@slack/logger@npm:4.0.0"
|
||||
dependencies:
|
||||
"@types/node": ">=18.0.0"
|
||||
checksum: dc79e9d2032c4bf9ce01d96cc72882f003dd376d036f172d4169662cfc2c9b384a80d5546b06021578dd473e7059f064303f0ba851eeb153387f2081a1e3062e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/oauth@npm:^2.6.3":
|
||||
version: 2.6.3
|
||||
resolution: "@slack/oauth@npm:2.6.3"
|
||||
dependencies:
|
||||
"@slack/logger": ^3.0.0
|
||||
"@slack/web-api": ^6.12.1
|
||||
"@types/jsonwebtoken": ^8.3.7
|
||||
"@types/node": ">=12"
|
||||
jsonwebtoken: ^9.0.0
|
||||
lodash.isstring: ^4.0.1
|
||||
checksum: 6b556da01bd2b026177b4074cd44bdeff00165fb4297ef8f350035ca79ababfff0c0993a297a46ab742bb97469c6c1c8f5790c328ecf6370290fe31014ba3c5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/socket-mode@npm:^1.3.6":
|
||||
version: 1.3.6
|
||||
resolution: "@slack/socket-mode@npm:1.3.6"
|
||||
dependencies:
|
||||
"@slack/logger": ^3.0.0
|
||||
"@slack/web-api": ^6.12.1
|
||||
"@types/node": ">=12.0.0"
|
||||
"@types/ws": ^7.4.7
|
||||
eventemitter3: ^5
|
||||
finity: ^0.5.4
|
||||
ws: ^7.5.3
|
||||
checksum: a84c15a6d25a21f76258d1ccebeec1d78b0a0dac0b02ffdfcb3596e7acda5459e4b99a42207eab7e57bed7a2a1d85ac173adf5e07aa66949eac9cc9df3b43947
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0, @slack/types@npm:^2.9.0":
|
||||
version: 2.14.0
|
||||
resolution: "@slack/types@npm:2.14.0"
|
||||
checksum: fbef74d50d0de8f16125f7178bd2e664a69eeefd827b09c6f78153957278fc4400049685c756076e7dbcabd04c22730ac783bcc5d36fd588c7749d35f02c2afd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/web-api@npm:^6.12.1, @slack/web-api@npm:^6.13.0":
|
||||
version: 6.13.0
|
||||
resolution: "@slack/web-api@npm:6.13.0"
|
||||
dependencies:
|
||||
"@slack/logger": ^3.0.0
|
||||
"@slack/types": ^2.11.0
|
||||
"@types/is-stream": ^1.1.0
|
||||
"@types/node": ">=12.0.0"
|
||||
axios: ^1.7.4
|
||||
eventemitter3: ^3.1.0
|
||||
form-data: ^2.5.0
|
||||
is-electron: 2.2.2
|
||||
is-stream: ^1.1.0
|
||||
p-queue: ^6.6.1
|
||||
p-retry: ^4.0.0
|
||||
checksum: 77f0d506bbb011ae43d322e5152e8b1ec2b88aa01256da6b3c9ff8ce106d2284f887cad2d9f044e0fe34dc865d60f2bce1c6bb5c4117150ff71a7ef341f5dfeb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@slack/web-api@npm:^7.5.0":
|
||||
version: 7.8.0
|
||||
resolution: "@slack/web-api@npm:7.8.0"
|
||||
dependencies:
|
||||
"@slack/logger": ^4.0.0
|
||||
"@slack/types": ^2.9.0
|
||||
"@types/node": ">=18.0.0"
|
||||
"@types/retry": 0.12.0
|
||||
axios: ^1.7.8
|
||||
eventemitter3: ^5.0.1
|
||||
form-data: ^4.0.0
|
||||
is-electron: 2.2.2
|
||||
is-stream: ^2
|
||||
p-queue: ^6
|
||||
p-retry: ^4
|
||||
retry: ^0.13.1
|
||||
checksum: d76fcb6cfe8a8ebdaf71aaee7dfef54f3ce4fb5958f9f841bcba095097349f649c47e5e79adbaabbd4051fe0c8d6d96445adeeab3d67416c161ae20868f637f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/abort-controller@npm:^3.1.2, @smithy/abort-controller@npm:^3.1.9":
|
||||
version: 3.1.9
|
||||
resolution: "@smithy/abort-controller@npm:3.1.9"
|
||||
@@ -19451,7 +19618,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/express@npm:*, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6":
|
||||
"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6":
|
||||
version: 4.17.21
|
||||
resolution: "@types/express@npm:4.17.21"
|
||||
dependencies:
|
||||
@@ -19613,6 +19780,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/is-stream@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@types/is-stream@npm:1.1.0"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 23fcb06cd8adc0124d4c44071bd4b447c41f5e4c2eccb6166789c7fc0992b566e2e8b628a3800ff4472b686d9085adbec203925068bf72e350e085650e83adec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@types/istanbul-lib-coverage@npm:2.0.1"
|
||||
@@ -19731,6 +19907,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsonwebtoken@npm:^8.3.7":
|
||||
version: 8.5.9
|
||||
resolution: "@types/jsonwebtoken@npm:8.5.9"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 33815ab02d1371b423118316b7706d2f2ec03eeee5e1494be72da50425d2384e5e0a09ea193f7a5ab4b4f6a9c5847147305f50e965f3d927a95bdf8adb471b2a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsonwebtoken@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "@types/jsonwebtoken@npm:9.0.0"
|
||||
@@ -19939,7 +20124,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0":
|
||||
"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.0.0":
|
||||
version: 22.13.10
|
||||
resolution: "@types/node@npm:22.13.10"
|
||||
dependencies:
|
||||
@@ -20166,6 +20351,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/promise.allsettled@npm:^1.0.3":
|
||||
version: 1.0.6
|
||||
resolution: "@types/promise.allsettled@npm:1.0.6"
|
||||
checksum: 07dca8da25b49c0dc323201095552d86159483dc910dc61c345357c9c196b8498e6be4bf260cc2a9a539a725108df61b53db1d82723ed9886bb7c72fedd65f14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.3":
|
||||
version: 15.7.14
|
||||
resolution: "@types/prop-types@npm:15.7.14"
|
||||
@@ -20370,6 +20562,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/retry@npm:0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "@types/retry@npm:0.12.0"
|
||||
checksum: 61a072c7639f6e8126588bf1eb1ce8835f2cb9c2aba795c4491cf6310e013267b0c8488039857c261c387e9728c1b43205099223f160bb6a76b4374f741b5603
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/retry@npm:0.12.2":
|
||||
version: 0.12.2
|
||||
resolution: "@types/retry@npm:0.12.2"
|
||||
@@ -20665,6 +20864,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tsscmp@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "@types/tsscmp@npm:1.0.2"
|
||||
checksum: c02c0bb9f14f550947fea9fa6f9f3c28e6b2d47a6d049a5450ed466fb0c8a685b6ff37d070d4c43d930a5affc9d828f5e16e35cde1e734de228ffd2df76ac2a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:*, @types/unist@npm:^2.0.0":
|
||||
version: 2.0.6
|
||||
resolution: "@types/unist@npm:2.0.6"
|
||||
@@ -20741,6 +20947,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^7.4.7":
|
||||
version: 7.4.7
|
||||
resolution: "@types/ws@npm:7.4.7"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/xml-encryption@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@types/xml-encryption@npm:1.2.4"
|
||||
@@ -22946,6 +23161,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"array.prototype.map@npm:^1.0.5":
|
||||
version: 1.0.8
|
||||
resolution: "array.prototype.map@npm:1.0.8"
|
||||
dependencies:
|
||||
call-bind: ^1.0.8
|
||||
call-bound: ^1.0.3
|
||||
define-properties: ^1.2.1
|
||||
es-abstract: ^1.23.6
|
||||
es-array-method-boxes-properly: ^1.0.0
|
||||
es-object-atoms: ^1.0.0
|
||||
is-string: ^1.1.1
|
||||
checksum: df321613636ec8461965d72421569ece78f269460535ced5ec88db9aaa4fc58a9f26e597d72e726f105c55fa4b4b6db0d3156489dc13dfbc7a098b4f1d17b5ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"array.prototype.tosorted@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "array.prototype.tosorted@npm:1.1.4"
|
||||
@@ -23300,7 +23530,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.7.7":
|
||||
"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.7.7, axios@npm:^1.7.8":
|
||||
version: 1.8.4
|
||||
resolution: "axios@npm:1.8.4"
|
||||
dependencies:
|
||||
@@ -24344,13 +24574,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "call-bind-apply-helpers@npm:1.0.1"
|
||||
"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
||||
dependencies:
|
||||
es-errors: ^1.3.0
|
||||
function-bind: ^1.1.2
|
||||
checksum: 3c55343261bb387c58a4762d15ad9d42053659a62681ec5eb50690c6b52a4a666302a01d557133ce6533e8bd04530ee3b209f23dd06c9577a1925556f8fcccdf
|
||||
checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -27648,6 +27878,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-array-method-boxes-properly@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "es-array-method-boxes-properly@npm:1.0.0"
|
||||
checksum: 2537fcd1cecf187083890bc6f5236d3a26bf39237433587e5bf63392e88faae929dbba78ff0120681a3f6f81c23fe3816122982c160d63b38c95c830b633b826
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "es-define-property@npm:1.0.1"
|
||||
@@ -27662,6 +27899,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-get-iterator@npm:^1.0.2":
|
||||
version: 1.1.3
|
||||
resolution: "es-get-iterator@npm:1.1.3"
|
||||
dependencies:
|
||||
call-bind: ^1.0.2
|
||||
get-intrinsic: ^1.1.3
|
||||
has-symbols: ^1.0.3
|
||||
is-arguments: ^1.1.1
|
||||
is-map: ^2.0.2
|
||||
is-set: ^2.0.2
|
||||
is-string: ^1.0.7
|
||||
isarray: ^2.0.5
|
||||
stop-iteration-iterator: ^1.0.0
|
||||
checksum: 8fa118da42667a01a7c7529f8a8cca514feeff243feec1ce0bb73baaa3514560bd09d2b3438873cf8a5aaec5d52da248131de153b28e2638a061b6e4df13267d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-iterator-helpers@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "es-iterator-helpers@npm:1.2.1"
|
||||
@@ -27693,12 +27947,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-object-atoms@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "es-object-atoms@npm:1.0.0"
|
||||
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "es-object-atoms@npm:1.1.1"
|
||||
dependencies:
|
||||
es-errors: ^1.3.0
|
||||
checksum: 26f0ff78ab93b63394e8403c353842b2272836968de4eafe97656adfb8a7c84b9099bf0fe96ed58f4a4cddc860f6e34c77f91649a58a5daa4a9c40b902744e3c
|
||||
checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -28539,7 +28793,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^5.0.1":
|
||||
"eventemitter3@npm:^5, eventemitter3@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "eventemitter3@npm:5.0.1"
|
||||
checksum: 543d6c858ab699303c3c32e0f0f47fc64d360bf73c3daf0ac0b5079710e340d6fe9f15487f94e66c629f5f82cd1a8678d692f3dbb6f6fcd1190e1b97fcad36f8
|
||||
@@ -28984,7 +29238,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"express@npm:^4.14.0, express@npm:^4.17.1, express@npm:^4.18.1, express@npm:^4.18.2, express@npm:^4.19.2, express@npm:^4.21.2":
|
||||
"express@npm:^4.14.0, express@npm:^4.17.1, express@npm:^4.18.1, express@npm:^4.18.2, express@npm:^4.19.2, express@npm:^4.21.0, express@npm:^4.21.2":
|
||||
version: 4.21.2
|
||||
resolution: "express@npm:4.21.2"
|
||||
dependencies:
|
||||
@@ -29545,6 +29799,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"finity@npm:^0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "finity@npm:0.5.4"
|
||||
checksum: eeea74de356ba963231108c3f8e2de44b4114497389121d603f8c3e8316b8d0772ff06b731af08ef5d6ca6b0e3a0fffab452122eca48837a98a2f7e5548b6be2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"first-chunk-stream@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "first-chunk-stream@npm:2.0.0"
|
||||
@@ -30131,21 +30392,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6":
|
||||
version: 1.2.6
|
||||
resolution: "get-intrinsic@npm:1.2.6"
|
||||
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6":
|
||||
version: 1.3.0
|
||||
resolution: "get-intrinsic@npm:1.3.0"
|
||||
dependencies:
|
||||
call-bind-apply-helpers: ^1.0.1
|
||||
dunder-proto: ^1.0.0
|
||||
call-bind-apply-helpers: ^1.0.2
|
||||
es-define-property: ^1.0.1
|
||||
es-errors: ^1.3.0
|
||||
es-object-atoms: ^1.0.0
|
||||
es-object-atoms: ^1.1.1
|
||||
function-bind: ^1.1.2
|
||||
get-proto: ^1.0.1
|
||||
gopd: ^1.2.0
|
||||
has-symbols: ^1.1.0
|
||||
hasown: ^2.0.2
|
||||
math-intrinsics: ^1.0.0
|
||||
checksum: a7592a0b7f023a2e83c0121fa9449ca83780e370a5feeebe8452119474d148016e43b455049134ae7a683b9b11b93d3f65eac199a0ad452ab740d5f0c299de47
|
||||
math-intrinsics: ^1.1.0
|
||||
checksum: 301008e4482bb9a9cb49e132b88fee093bff373b4e6def8ba219b1e96b60158a6084f273ef5cafe832e42cd93462f4accb46a618d35fe59a2b507f2388c5b79d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -30170,6 +30431,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-proto@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "get-proto@npm:1.0.1"
|
||||
dependencies:
|
||||
dunder-proto: ^1.0.1
|
||||
es-object-atoms: ^1.0.0
|
||||
checksum: 4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-stdin@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "get-stdin@npm:9.0.0"
|
||||
@@ -32024,13 +32295,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-arguments@npm:^1.0.4":
|
||||
version: 1.1.1
|
||||
resolution: "is-arguments@npm:1.1.1"
|
||||
"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1":
|
||||
version: 1.2.0
|
||||
resolution: "is-arguments@npm:1.2.0"
|
||||
dependencies:
|
||||
call-bind: ^1.0.2
|
||||
has-tostringtag: ^1.0.0
|
||||
checksum: 7f02700ec2171b691ef3e4d0e3e6c0ba408e8434368504bb593d0d7c891c0dbfda6d19d30808b904a6cb1929bca648c061ba438c39f296c2a8ca083229c49f27
|
||||
call-bound: ^1.0.2
|
||||
has-tostringtag: ^1.0.2
|
||||
checksum: aae9307fedfe2e5be14aebd0f48a9eeedf6b8c8f5a0b66257b965146d1e94abdc3f08e3dce3b1d908e1fa23c70039a88810ee1d753905758b9b6eebbab0bafeb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -32192,6 +32463,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-electron@npm:2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "is-electron@npm:2.2.2"
|
||||
checksum: de5aa8bd8d72c96675b8d0f93fab4cc21f62be5440f65bc05c61338ca27bd851a64200f31f1bf9facbaa01b3dbfed7997b2186741d84b93b63e0aff1db6a9494
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-elevated@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "is-elevated@npm:3.0.0"
|
||||
@@ -32322,7 +32600,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-map@npm:^2.0.3":
|
||||
"is-map@npm:^2.0.2, is-map@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "is-map@npm:2.0.3"
|
||||
checksum: e6ce5f6380f32b141b3153e6ba9074892bbbbd655e92e7ba5ff195239777e767a976dcd4e22f864accaf30e53ebf961ab1995424aef91af68788f0591b7396cc
|
||||
@@ -32528,7 +32806,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-set@npm:^2.0.3":
|
||||
"is-set@npm:^2.0.2, is-set@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "is-set@npm:2.0.3"
|
||||
checksum: 36e3f8c44bdbe9496c9689762cc4110f6a6a12b767c5d74c0398176aa2678d4467e3bf07595556f2dba897751bde1422480212b97d973c7b08a343100b0c0dfe
|
||||
@@ -32560,7 +32838,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1":
|
||||
"is-stream@npm:^2, is-stream@npm:^2.0.0, is-stream@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "is-stream@npm:2.0.1"
|
||||
checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66
|
||||
@@ -32934,6 +33212,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iterate-iterator@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "iterate-iterator@npm:1.0.2"
|
||||
checksum: 97b3ed4f2bebe038be57d03277879e406b2c537ceeeab7f82d4167f9a3cff872cc2cc5da3dc9920ff544ca247329d2a4d44121bb8ef8d0807a72176bdbc17c84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iterate-value@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "iterate-value@npm:1.0.2"
|
||||
dependencies:
|
||||
es-get-iterator: ^1.0.2
|
||||
iterate-iterator: ^1.0.1
|
||||
checksum: 446a4181657df1872e5020713206806757157db6ab375dee05eb4565b66e1244d7a99cd36ce06862261ad4bd059e66ba8192f62b5d1ff41d788c3b61953af6c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iterator.prototype@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "iterator.prototype@npm:1.1.4"
|
||||
@@ -35626,7 +35921,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0":
|
||||
"math-intrinsics@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "math-intrinsics@npm:1.1.0"
|
||||
checksum: 0e513b29d120f478c85a70f49da0b8b19bc638975eca466f2eeae0071f3ad00454c621bf66e16dd435896c208e719fc91ad79bbfba4e400fe0b372e7c1c9c9a2
|
||||
@@ -38457,7 +38752,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-queue@npm:^6.6.2":
|
||||
"p-queue@npm:^6, p-queue@npm:^6.6.1, p-queue@npm:^6.6.2":
|
||||
version: 6.6.2
|
||||
resolution: "p-queue@npm:6.6.2"
|
||||
dependencies:
|
||||
@@ -38467,6 +38762,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-retry@npm:^4, p-retry@npm:^4.0.0":
|
||||
version: 4.6.2
|
||||
resolution: "p-retry@npm:4.6.2"
|
||||
dependencies:
|
||||
"@types/retry": 0.12.0
|
||||
retry: ^0.13.1
|
||||
checksum: 45c270bfddaffb4a895cea16cb760dcc72bdecb6cb45fef1971fa6ea2e91ddeafddefe01e444ac73e33b1b3d5d29fb0dd18a7effb294262437221ddc03ce0f2e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-retry@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "p-retry@npm:6.2.0"
|
||||
@@ -40192,6 +40497,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise.allsettled@npm:^1.0.2":
|
||||
version: 1.0.7
|
||||
resolution: "promise.allsettled@npm:1.0.7"
|
||||
dependencies:
|
||||
array.prototype.map: ^1.0.5
|
||||
call-bind: ^1.0.2
|
||||
define-properties: ^1.2.0
|
||||
es-abstract: ^1.22.1
|
||||
get-intrinsic: ^1.2.1
|
||||
iterate-value: ^1.0.2
|
||||
checksum: 96186392286e5ab9aef1a1a725c061c8cf268b6cf141f151daa3834bb8e1680f3b159af6536ce59cf80d4a6a5ad1d8371d05759980cc6c90d58800ddb0a7c119
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise.series@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "promise.series@npm:0.2.0"
|
||||
@@ -40612,7 +40931,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:2.5.2, raw-body@npm:^2.4.1":
|
||||
"raw-body@npm:2.5.2, raw-body@npm:^2.3.3, raw-body@npm:^2.4.1":
|
||||
version: 2.5.2
|
||||
resolution: "raw-body@npm:2.5.2"
|
||||
dependencies:
|
||||
@@ -40934,11 +41253,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.12.2":
|
||||
version: 7.53.2
|
||||
resolution: "react-hook-form@npm:7.53.2"
|
||||
version: 7.55.0
|
||||
resolution: "react-hook-form@npm:7.55.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
checksum: 37e2dd0e79cd8d3376a7a2cb72ad7b59f0594be499daa898d2c6bec896fc30c2f86e62e8b41bc9d325f77220bd8d76cb31e917f77f1c92ad5740adb1a4cc69e2
|
||||
checksum: ff37fba242c0e94911bf8515bfe1b0670f93bf0ed0c44c923108859244f41da3f0f6dffcf70d570b152ef6b1d56936d943b45fba11c63f21b913fba8f0da862b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -43875,6 +44194,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stop-iteration-iterator@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "stop-iteration-iterator@npm:1.1.0"
|
||||
dependencies:
|
||||
es-errors: ^1.3.0
|
||||
internal-slot: ^1.1.0
|
||||
checksum: be944489d8829fb3bdec1a1cc4a2142c6b6eb317305eeace1ece978d286d6997778afa1ae8cb3bd70e2b274b9aa8c69f93febb1e15b94b1359b11058f9d3c3a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stoppable@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "stoppable@npm:1.1.0"
|
||||
@@ -45517,7 +45846,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsscmp@npm:1.0.6":
|
||||
"tsscmp@npm:1.0.6, tsscmp@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "tsscmp@npm:1.0.6"
|
||||
checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf
|
||||
@@ -47449,7 +47778,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^7, ws@npm:^7.5.5":
|
||||
"ws@npm:^7, ws@npm:^7.5.3, ws@npm:^7.5.5":
|
||||
version: 7.5.10
|
||||
resolution: "ws@npm:7.5.10"
|
||||
peerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user