Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
import catalogGraphPlugin from '@backstage/plugin-catalog-graph/alpha';
import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha';
import apiDocsPlugin from '@backstage/plugin-api-docs/alpha';
import notificationsPlugin from '@backstage/plugin-notifications/alpha';
import signalsPlugin from '@backstage/plugin-signals/alpha';
import githubActionsPlugin from '@backstage-community/plugin-github-actions/alpha';
import { createApp } from '@backstage/frontend-defaults';
Expand Down Expand Up @@ -41,6 +42,7 @@ const app = createApp({
gsPlugin,
scaffolderPlugin,
scaffolderPluginOverrides,
notificationsPlugin,
signalsPlugin,
// Upstream NFS plugins (pages provided by routeOverrides or defaults):
homePlugin,
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/modules/nav/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
UserSettingsSignInAvatar,
Settings as SidebarSettings,
} from '@backstage/plugin-user-settings';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';

export const SidebarContent = NavContentBlueprint.make({
params: {
Expand Down Expand Up @@ -50,6 +51,7 @@ export const SidebarContent = NavContentBlueprint.make({
{nav.take('page:scaffolder')}
</SidebarGroup>
<SidebarSpace />
<NotificationsSidebarItem />
<SidebarDivider />
<SidebarGroup
label="Settings"
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@giantswarm/backstage-plugin-auth-backend-module-gs": "^0.13.0",
"@giantswarm/backstage-plugin-catalog-backend-module-gs": "^0.2.0",
"@giantswarm/backstage-plugin-gs-backend": "^0.6.0",
"@giantswarm/backstage-plugin-notifications-backend-module-gs": "workspace:^",
"@giantswarm/backstage-plugin-scaffolder-backend-module-gs": "^0.11.0",
"@giantswarm/backstage-plugin-techdocs-backend-module-gs": "^0.10.0",
"@internal/backend-common": "workspace:^",
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ backend.add(import('@backstage/plugin-kubernetes-backend'));
// notifications and signals plugins
backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('@backstage/plugin-signals-backend'));
backend.add(
import('@giantswarm/backstage-plugin-notifications-backend-module-gs'),
);

// giantswarm plugin
backend.add(import('@giantswarm/backstage-plugin-gs-backend'));
Expand Down
41 changes: 41 additions & 0 deletions plugins/notifications-backend-module-gs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@giantswarm/backstage-plugin-notifications-backend-module-gs",
"description": "A backend module for the notifications plugin that sends release notifications on startup.",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/plugin-notifications-node": "backstage:^",
"semver": "^7.7.3"
},
"devDependencies": {
"@backstage/backend-test-utils": "backstage:^",
"@backstage/cli": "backstage:^",
"@types/semver": "^7"
},
"files": [
"dist"
],
"bugs": "https://github.com/giantswarm/backstage/issues",
"author": "Giant Swarm"
}
1 change: 1 addition & 0 deletions plugins/notifications-backend-module-gs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { notificationsModuleGsReleaseNotifier as default } from './module';
31 changes: 31 additions & 0 deletions plugins/notifications-backend-module-gs/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { notificationService } from '@backstage/plugin-notifications-node';
import { sendReleaseNotification } from './releaseNotifier';

export const notificationsModuleGsReleaseNotifier = createBackendModule({
pluginId: 'notifications',
moduleId: 'gs-release-notifier',
register(reg) {
reg.registerInit({
deps: {
notifications: notificationService,
lifecycle: coreServices.lifecycle,
logger: coreServices.logger,
},
async init({ notifications, lifecycle, logger }) {
const version = process.env.npm_package_version ?? '';

lifecycle.addStartupHook(async () => {
await sendReleaseNotification({
version,
notifications,
logger,
});
});
},
});
},
});
102 changes: 102 additions & 0 deletions plugins/notifications-backend-module-gs/src/releaseNotifier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { sendReleaseNotification } from './releaseNotifier';
import { NotificationService } from '@backstage/plugin-notifications-node';
import { LoggerService } from '@backstage/backend-plugin-api';

function createMockLogger(): jest.Mocked<LoggerService> {
return {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
child: jest.fn().mockReturnThis(),
};
}

function createMockNotifications(): jest.Mocked<NotificationService> {
return {
send: jest.fn(),
};
}

describe('sendReleaseNotification', () => {
let logger: jest.Mocked<LoggerService>;
let notifications: jest.Mocked<NotificationService>;

beforeEach(() => {
logger = createMockLogger();
notifications = createMockNotifications();
jest.clearAllMocks();
});

it('skips when version is empty', async () => {
await sendReleaseNotification({ version: '', notifications, logger });

expect(notifications.send).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('not available'),
);
});

it('skips when version is unparseable', async () => {
await sendReleaseNotification({
version: 'not-a-version',
notifications,
logger,
});

expect(notifications.send).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('could not parse'),
);
});

it('skips patch releases', async () => {
await sendReleaseNotification({
version: '0.115.1',
notifications,
logger,
});

expect(notifications.send).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('patch release'),
);
});

it('sends notification for minor release with link', async () => {
await sendReleaseNotification({
version: '0.115.0',
notifications,
logger,
});

expect(notifications.send).toHaveBeenCalledWith({
recipients: { type: 'broadcast' },
payload: {
title: 'Portal updated to v0.115.0',
description: 'Click to view the release notes.',
link: 'https://github.com/giantswarm/backstage/releases/tag/v0.115.0',
severity: 'normal',
topic: 'release',
scope: 'release-v0.115.0',
},
});
});

it('sends notification for major release', async () => {
await sendReleaseNotification({
version: '1.0.0',
notifications,
logger,
});

expect(notifications.send).toHaveBeenCalledWith({
recipients: { type: 'broadcast' },
payload: expect.objectContaining({
title: 'Portal updated to v1.0.0',
link: 'https://github.com/giantswarm/backstage/releases/tag/v1.0.0',
scope: 'release-v1.0.0',
}),
});
});
});
55 changes: 55 additions & 0 deletions plugins/notifications-backend-module-gs/src/releaseNotifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import semver from 'semver';
import { LoggerService } from '@backstage/backend-plugin-api';
import { NotificationService } from '@backstage/plugin-notifications-node';

const RELEASE_URL_BASE = 'https://github.com/giantswarm/backstage/releases/tag';

export interface ReleaseNotifierOptions {
version: string;
notifications: NotificationService;
logger: LoggerService;
}

export async function sendReleaseNotification(
options: ReleaseNotifierOptions,
): Promise<void> {
const { version, notifications, logger } = options;

if (!version) {
logger.warn('Release notifier: app version not available, skipping.');
return;
}

const parsed = semver.parse(version);
if (!parsed) {
logger.warn(
`Release notifier: could not parse version "${version}", skipping.`,
);
return;
}

if (parsed.patch !== 0) {
logger.info(
`Release notifier: patch release v${version} — skipping notification.`,
);
return;
}

const scope = `release-v${parsed.major}.${parsed.minor}.0`;

logger.info(
`Release notifier: sending broadcast notification for v${version}.`,
);

await notifications.send({
recipients: { type: 'broadcast' },
payload: {
title: `Portal updated to v${version}`,
description: 'Click to view the release notes.',
link: `${RELEASE_URL_BASE}/v${version}`,
severity: 'normal',
topic: 'release',
scope,
},
});
}
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7107,7 +7107,7 @@ __metadata:
languageName: node
linkType: hard

"@backstage/plugin-notifications-node@npm:^0.2.24":
"@backstage/plugin-notifications-node@backstage:^::backstage=1.49.2&npm=0.2.24, @backstage/plugin-notifications-node@npm:^0.2.24":
version: 0.2.24
resolution: "@backstage/plugin-notifications-node@npm:0.2.24"
dependencies:
Expand Down Expand Up @@ -10109,6 +10109,19 @@ __metadata:
languageName: unknown
linkType: soft

"@giantswarm/backstage-plugin-notifications-backend-module-gs@workspace:^, @giantswarm/backstage-plugin-notifications-backend-module-gs@workspace:plugins/notifications-backend-module-gs":
version: 0.0.0-use.local
resolution: "@giantswarm/backstage-plugin-notifications-backend-module-gs@workspace:plugins/notifications-backend-module-gs"
dependencies:
"@backstage/backend-plugin-api": "backstage:^"
"@backstage/backend-test-utils": "backstage:^"
"@backstage/cli": "backstage:^"
"@backstage/plugin-notifications-node": "backstage:^"
"@types/semver": "npm:^7"
semver: "npm:^7.7.3"
languageName: unknown
linkType: soft

"@giantswarm/backstage-plugin-scaffolder-backend-module-gs@npm:^0.11.0, @giantswarm/backstage-plugin-scaffolder-backend-module-gs@workspace:plugins/scaffolder-backend-module-gs":
version: 0.0.0-use.local
resolution: "@giantswarm/backstage-plugin-scaffolder-backend-module-gs@workspace:plugins/scaffolder-backend-module-gs"
Expand Down Expand Up @@ -23554,6 +23567,7 @@ __metadata:
"@giantswarm/backstage-plugin-auth-backend-module-gs": "npm:^0.13.0"
"@giantswarm/backstage-plugin-catalog-backend-module-gs": "npm:^0.2.0"
"@giantswarm/backstage-plugin-gs-backend": "npm:^0.6.0"
"@giantswarm/backstage-plugin-notifications-backend-module-gs": "workspace:^"
"@giantswarm/backstage-plugin-scaffolder-backend-module-gs": "npm:^0.11.0"
"@giantswarm/backstage-plugin-techdocs-backend-module-gs": "npm:^0.10.0"
"@internal/backend-common": "workspace:^"
Expand Down