feat: add new scaffolder action to send notifications

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-05-02 13:12:13 +03:00
parent 32cc4e6468
commit 503d769eb9
14 changed files with 433 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-notifications': patch
---
Add a new scaffolder action to allow sending notifications from templates
+1
View File
@@ -61,6 +61,7 @@ yarn.lock @backstage/maintainers @backst
/plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin
/plugins/notifications @backstage/maintainers @backstage/notifications-maintainers
/plugins/notifications-* @backstage/maintainers @backstage/notifications-maintainers
/plugins/scaffolder-backend-module-notifications @backstage/maintainers @backstage/notifications-maintainers
/plugins/octopus-deploy @backstage/maintainers @backstage/reviewers @jmezach
/plugins/permission-* @backstage/permission-maintainers
/plugins/playlist @backstage/maintainers @backstage/reviewers @kuangp
+1
View File
@@ -50,6 +50,7 @@
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-notifications": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-rails": "workspace:^",
"@backstage/plugin-search-backend": "workspace:^",
"@backstage/plugin-search-backend-module-catalog": "workspace:^",
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,5 @@
# @backstage/plugin-scaffolder-backend-module-notifications
The notifications backend module for the scaffolder plugin.
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,32 @@
## API Report File for "@backstage/plugin-scaffolder-backend-module-notifications"
> 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';
import { JsonObject } from '@backstage/types';
import { NotificationService } from '@backstage/plugin-notifications-node';
import { NotificationSeverity } from '@backstage/plugin-notifications-common';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
// @public (undocumented)
export function createSendNotificationAction(options: {
notifications: NotificationService;
}): TemplateAction<
{
recipients: string;
entityRefs?: string[] | undefined;
title: string;
info?: string | undefined;
link?: string | undefined;
severity?: NotificationSeverity | undefined;
scope?: string | undefined;
optional?: boolean | undefined;
},
JsonObject
>;
// @public
const scaffolderModuleNotifications: () => BackendFeature;
export default scaffolderModuleNotifications;
```
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-scaffolder-backend-module-notifications
title: '@backstage/plugin-scaffolder-backend-module-notifications'
description: The notifications module for @backstage/plugin-scaffolder-backend
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,45 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-notifications",
"version": "0.0.0",
"description": "The notifications backend module for the scaffolder plugin.",
"backstage": {
"role": "backend-plugin-module"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/scaffolder-backend-module-notifications"
},
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"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-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-notifications-node": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"octokit": "^3.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
}
}
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { createSendNotificationAction } from './sendNotification';
@@ -0,0 +1,93 @@
/*
* 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 { createSendNotificationAction } from './sendNotification';
import { NotificationService } from '@backstage/plugin-notifications-node';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
describe('notification:send', () => {
const notificationService: jest.Mocked<NotificationService> = {
send: jest.fn(),
};
let action: TemplateAction<any>;
beforeEach(() => {
jest.resetAllMocks();
action = createSendNotificationAction({
notifications: notificationService,
});
});
const mockContext = createMockActionContext({
input: {
recipients: 'broadcast',
title: 'Test notification',
},
});
it('should send broadcast notification', async () => {
const ctx = Object.assign({}, mockContext, {
input: { recipients: 'broadcast', title: 'Test notification' },
});
await action.handler(ctx);
expect(notificationService.send).toHaveBeenCalledWith({
recipients: { type: 'broadcast' },
payload: {
title: 'Test notification',
},
});
});
it('should send entity notification', async () => {
const ctx = Object.assign({}, mockContext, {
input: {
recipients: 'entity',
entityRefs: ['user:default/john.doe'],
title: 'Test notification',
},
});
await action.handler(ctx);
expect(notificationService.send).toHaveBeenCalledWith({
recipients: { type: 'entity', entityRef: ['user:default/john.doe'] },
payload: {
title: 'Test notification',
},
});
});
it('should throw error if entity refs are missing', async () => {
const ctx = Object.assign({}, mockContext, {
input: {
recipients: 'entity',
title: 'Test notification',
},
});
await expect(action.handler(ctx)).rejects.toThrow();
});
it('should not throw error if entity refs are missing but optional is true', async () => {
const ctx = Object.assign({}, mockContext, {
input: {
recipients: 'entity',
title: 'Test notification',
optional: true,
},
});
await expect(action.handler(ctx)).resolves.not.toThrow();
expect(notificationService.send).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,146 @@
/*
* 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 {
NotificationRecipients,
NotificationService,
} from '@backstage/plugin-notifications-node';
import {
NotificationPayload,
NotificationSeverity,
} from '@backstage/plugin-notifications-common';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
/**
* @public
*/
export function createSendNotificationAction(options: {
notifications: NotificationService;
}) {
const { notifications } = options;
return createTemplateAction<{
recipients: string;
entityRefs?: string[];
title: string;
info?: string;
link?: string;
severity?: NotificationSeverity;
scope?: string;
optional?: boolean;
}>({
id: 'notification:send',
description: 'Sends a notification using NotificationService',
schema: {
input: {
type: 'object',
required: ['recipients', 'title'],
properties: {
recipients: {
title: 'Recipient',
enum: ['broadcast', 'entity'],
description:
'The recipient of the notification, either broadcast or entity. If using entity, also entityRef must be provided',
type: 'string',
},
entityRefs: {
title: 'Entity references',
description:
'The entity references to send the notification to, required if using recipient of entity',
type: 'array',
items: {
type: 'string',
},
},
title: {
title: 'Title',
description: 'Notification title',
type: 'string',
},
info: {
title: 'Description',
description: 'Notification description',
type: 'string',
},
link: {
title: 'Link',
description: 'Notification link',
type: 'string',
},
severity: {
title: 'Severity',
type: 'string',
description: `Notification severity`,
enum: ['low', 'normal', 'high', 'critical'],
},
scope: {
title: 'Scope',
description: 'Notification scope',
type: 'string',
},
optional: {
title: 'Optional',
description:
'Do not fail the action if the notification sending fails',
type: 'boolean',
},
},
},
},
async handler(ctx) {
const {
recipients,
entityRefs,
title,
info,
link,
severity,
scope,
optional,
} = ctx.input;
ctx.logger.info(`Sending notification to ${recipients}`);
if (recipients === 'entity' && !entityRefs) {
if (optional !== true) {
throw new Error('Entity references must be provided');
}
return;
}
const notificationRecipients: NotificationRecipients =
recipients === 'broadcast'
? { type: 'broadcast' }
: { type: 'entity', entityRef: entityRefs! };
const payload: NotificationPayload = {
title,
description: info,
link,
severity,
scope,
};
try {
await notifications.send({
recipients: notificationRecipients,
payload,
});
} catch (e) {
ctx.logger.error(`Failed to send notification: ${e}`);
if (optional !== true) {
throw e;
}
}
},
});
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* The notifications backend module for the scaffolder plugin.
*
* @packageDocumentation
*/
export * from './actions';
export { scaffolderModuleNotifications as default } from './module';
@@ -0,0 +1,39 @@
/*
* 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 { createBackendModule } from '@backstage/backend-plugin-api';
import { notificationService } from '@backstage/plugin-notifications-node';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createSendNotificationAction } from './actions';
/**
* @public
* The Notifications module for the Scaffolder Backend
*/
export const scaffolderModuleNotifications = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'notifications',
register(reg) {
reg.registerInit({
deps: {
notifications: notificationService,
scaffolder: scaffolderActionsExtensionPoint,
},
async init({ notifications, scaffolder }) {
scaffolder.addActions(createSendNotificationAction({ notifications }));
},
});
},
});
+16
View File
@@ -6695,6 +6695,21 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-notifications@workspace:^, @backstage/plugin-scaffolder-backend-module-notifications@workspace:plugins/scaffolder-backend-module-notifications":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-notifications@workspace:plugins/scaffolder-backend-module-notifications"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-notifications-node": "workspace:^"
"@backstage/plugin-scaffolder-node": "workspace:^"
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^"
octokit: ^3.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-rails@workspace:^, @backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails"
@@ -24249,6 +24264,7 @@ __metadata:
"@backstage/plugin-scaffolder-backend": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-notifications": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-rails": "workspace:^"
"@backstage/plugin-search-backend": "workspace:^"
"@backstage/plugin-search-backend-module-catalog": "workspace:^"