Skip to content

Commit 8604b52

Browse files
updated docs site to include new slackbot ai summary feature
1 parent a71ba9f commit 8604b52

11 files changed

Lines changed: 245 additions & 45 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"engines": {
1010
"node": ">=22"
1111
},
12-
"packageManager": "pnpm@10.30.3",
12+
"packageManager": "pnpm@10.33.0",
1313
"devDependencies": {
1414
"@changesets/cli": "^2.30.0",
1515
"@types/node": "25.6.0",

packages/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
},
1111
"dependencies": {
1212
"@astrojs/starlight": "^0.38.3",
13-
"astro": "^6.1.5"
13+
"astro": "^6.1.6"
1414
}
1515
}

packages/docs/src/content/docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { CardGrid, LinkCard } from '@astrojs/starlight/components';
2727
<LinkCard
2828
title="Slack Bot"
2929
href="./slack-bot"
30-
description="Get update notifications in Slack when tracked packages release new versions."
30+
description="Get Slack release alerts with GitHub release links and AI-generated summaries for tracked packages."
3131
/>
3232
<LinkCard
3333
title="VS Code Extension"

packages/docs/src/content/docs/introduction.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Patch Pulse is a suite of tools that help you keep your npm dependencies up to d
1111
| --------------------------------------- | ---------------------------------------------------------------------------- |
1212
| [CLI](../cli/overview) | Zero-dependency command-line tool to scan your project for outdated packages |
1313
| [VS Code Extension](../vscode/overview) | Inline version information directly in your `package.json` files |
14-
| [Slack Bot](../slack-bot) | Get notified in Slack when packages you depend on release new versions |
14+
| [Slack Bot](../slack-bot) | Get Slack release alerts with GitHub release links and AI summaries |
1515

1616
## Core concepts
1717

packages/docs/src/content/docs/slack-bot.mdx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
title: Slack Bot
3-
description: Get notified in Slack when packages you depend on release new versions.
3+
description: Get Slack notifications for new package releases, plus GitHub release links and AI summaries.
44
---
55

66
import { Aside } from '@astrojs/starlight/components';
77

8-
The Patch Pulse Slack Bot notifies your workspace whenever a package you depend on releases a new version — so your team stays informed without having to run the CLI manually.
8+
The Patch Pulse Slack Bot notifies your workspace whenever a package you depend on releases a new version, updates the message with GitHub release links when they are available, and posts an AI-generated summary in the thread when the release evidence is strong enough.
99

1010
<a
1111
href="https://grand-yak-92.convex.site/slack/install"
@@ -28,3 +28,10 @@ The Patch Pulse Slack Bot notifies your workspace whenever a package you depend
2828
## How it works
2929

3030
Once installed, the bot monitors packages you care about and sends a Slack notification when a new version is published to the npm registry.
31+
32+
For supported releases, Patch Pulse also enriches that notification after the initial post:
33+
34+
- it adds GitHub release links when repository metadata can be resolved
35+
- it posts a short AI-generated thread summary of what changed when upstream release evidence is strong enough
36+
37+
That gives your team the version bump, the release source, and a quick readable summary without leaving Slack.

packages/notifier-bot/convex/packages.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,18 @@ export const ensureExists = internalMutation({
6060
name: v.string(),
6161
version: v.string(),
6262
ecosystem: v.optional(v.string()),
63+
githubRepoUrl: v.optional(v.string()),
6364
},
64-
handler: async (ctx, { name, version, ecosystem }) => {
65+
handler: async (ctx, { name, version, ecosystem, githubRepoUrl }) => {
6566
const existing = await ctx.db
6667
.query('packages')
6768
.withIndex('by_name', (q) => q.eq('name', name))
6869
.first();
6970

7071
if (existing) {
72+
if (githubRepoUrl && existing.githubRepoUrl !== githubRepoUrl) {
73+
await ctx.db.patch(existing._id, { githubRepoUrl });
74+
}
7175
return { packageId: existing._id, dbVersion: existing.currentVersion };
7276
}
7377

@@ -76,6 +80,7 @@ export const ensureExists = internalMutation({
7680
currentVersion: version,
7781
ecosystem: ecosystem ?? 'npm',
7882
lastChecked: Date.now(),
83+
githubRepoUrl,
7984
});
8085

8186
return { packageId, dbVersion: version };

packages/notifier-bot/convex/slack.commands.test.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ function createFetchMock(
1919
responseMessages: string[],
2020
options?: {
2121
npmVersions?: Record<string, string>;
22+
npmRepositories?: Record<string, string | null>;
2223
postedMessages?: Array<{ channel: string; text: string }>;
2324
publishedViews?: Array<{ userId: string; blocks: unknown[] }>;
2425
channelNames?: Record<string, string>;
2526
channelIdsByName?: Record<string, string>;
2627
conversationsListError?: string;
28+
githubReleaseUrlsByTag?: Record<string, string>;
2729
},
2830
) {
2931
return vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
@@ -43,13 +45,24 @@ function createFetchMock(
4345
return jsonResponse({ error: 'not_found' }, 404);
4446
}
4547

48+
const repository = options?.npmRepositories?.[packageName];
4649
return jsonResponse({
4750
'dist-tags': { latest: version },
48-
repository: 'github:facebook/react',
51+
...(repository ? { repository } : {}),
4952
versions: { [version]: {} },
5053
});
5154
}
5255

56+
if (url.startsWith('https://api.github.com/repos/')) {
57+
const match = url.match(/\/releases\/tags\/([^/?#]+)$/);
58+
const tag = match ? decodeURIComponent(match[1]) : '';
59+
const htmlUrl = tag ? options?.githubReleaseUrlsByTag?.[tag] : undefined;
60+
if (!htmlUrl) {
61+
return jsonResponse({ message: 'Not Found' }, 404);
62+
}
63+
return jsonResponse({ html_url: htmlUrl });
64+
}
65+
5366
if (url === 'https://slack.com/api/chat.postMessage') {
5467
const raw = typeof init?.body === 'string' ? init.body : '';
5568
const body = JSON.parse(raw);
@@ -390,6 +403,63 @@ describe('Slack multi-channel subscriptions', () => {
390403
expect(responseMessages).toEqual([]);
391404
});
392405

406+
it('links the tracked current version to the exact GitHub release tag when available', async () => {
407+
const postedMessages: Array<{ channel: string; text: string }> = [];
408+
vi.stubGlobal(
409+
'fetch',
410+
createFetchMock([], {
411+
postedMessages,
412+
npmVersions: { react: '19.0.0' },
413+
npmRepositories: { react: 'github:facebook/react' },
414+
githubReleaseUrlsByTag: {
415+
'v19.0.0': 'https://github.com/facebook/react/releases/tag/v19.0.0',
416+
},
417+
}),
418+
);
419+
420+
const t = convexTest(schema, modules);
421+
await seedWorkspace(t);
422+
423+
await t.action(internal.slack.commands.processNpmTrack, {
424+
packageName: 'react',
425+
teamId: TEAM_ID,
426+
minUpdateType: 'patch',
427+
userId: 'U_ALICE',
428+
});
429+
430+
expect(postedMessages).toHaveLength(1);
431+
expect(postedMessages[0].text).toContain(
432+
'current version <https://github.com/facebook/react/releases/tag/v19.0.0|19.0.0>',
433+
);
434+
});
435+
436+
it('falls back to the repo releases page when tracking cannot resolve an exact release tag', async () => {
437+
const postedMessages: Array<{ channel: string; text: string }> = [];
438+
vi.stubGlobal(
439+
'fetch',
440+
createFetchMock([], {
441+
postedMessages,
442+
npmVersions: { react: '19.0.0' },
443+
npmRepositories: { react: 'github:facebook/react' },
444+
}),
445+
);
446+
447+
const t = convexTest(schema, modules);
448+
await seedWorkspace(t);
449+
450+
await t.action(internal.slack.commands.processNpmTrack, {
451+
packageName: 'react',
452+
teamId: TEAM_ID,
453+
minUpdateType: 'patch',
454+
userId: 'U_ALICE',
455+
});
456+
457+
expect(postedMessages).toHaveLength(1);
458+
expect(postedMessages[0].text).toContain(
459+
'current version <https://github.com/facebook/react/releases|19.0.0>',
460+
);
461+
});
462+
393463
it('resolves a typed channel name to a Slack channel ID before posting and storing', async () => {
394464
const responseMessages: string[] = [];
395465
const postedMessages: Array<{ channel: string; text: string }> = [];
@@ -574,7 +644,12 @@ describe('Slack multi-channel subscriptions', () => {
574644

575645
it('stores github repo metadata during polling so list can use it later', async () => {
576646
const responseMessages: string[] = [];
577-
vi.stubGlobal('fetch', createFetchMock(responseMessages));
647+
vi.stubGlobal(
648+
'fetch',
649+
createFetchMock(responseMessages, {
650+
npmRepositories: { react: 'github:facebook/react' },
651+
}),
652+
);
578653

579654
const t = convexTest(schema, modules);
580655
const subscriberId = await seedWorkspace(t);

packages/notifier-bot/convex/slack/commands.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { v } from 'convex/values';
2-
import { fetchNpmLatestVersion, isVersionOutdated } from '@patch-pulse/shared';
2+
import {
3+
fetchNpmPackageManifest,
4+
getNpmLatestVersion,
5+
isVersionOutdated,
6+
} from '@patch-pulse/shared';
37
import { ActionCtx, httpAction, internalAction } from '../_generated/server';
48
import { internal } from '../_generated/api';
59
import { Id } from '../_generated/dataModel';
6-
import { formatSlackPackageLink, formatSlackVersionText } from './links';
10+
import {
11+
extractGitHubRepoUrl,
12+
formatSlackPackageLink,
13+
formatSlackVersionText,
14+
resolveSlackVersionText,
15+
} from './links';
716
import {
817
chatPostMessage,
918
conversationsFindByName,
@@ -33,31 +42,31 @@ function formatPackageName(packageName: string): string {
3342
return `\`${packageName}\``;
3443
}
3544

36-
function formatVersion(version: string): string {
37-
return `\`${version}\``;
38-
}
39-
4045
type TrackOutcome =
4146
| {
4247
kind: 'tracked';
4348
packageId: Id<'packages'>;
4449
packageName: string;
4550
version: string;
4651
displayVersion: string;
52+
versionText: string;
53+
displayVersionText: string;
4754
filterLabel: string | null;
4855
pendingUpdate: boolean;
4956
}
5057
| {
5158
kind: 'updated';
5259
packageName: string;
5360
version: string;
61+
versionText: string;
5462
filterLabel: string | null;
5563
channelName?: string;
5664
}
5765
| {
5866
kind: 'already';
5967
packageName: string;
6068
version: string;
69+
versionText: string;
6170
filterLabel: string | null;
6271
channelName?: string;
6372
}
@@ -86,20 +95,24 @@ async function trackPackage(
8695
): Promise<TrackOutcome> {
8796
packageName = normalizeNpmPackageName(packageName);
8897

89-
const version = await fetchNpmLatestVersion(packageName, {
98+
const manifest = await fetchNpmPackageManifest(packageName, {
9099
userAgent: 'patch-pulse-notifier-bot',
91100
}).catch(() => null);
101+
const version = getNpmLatestVersion(manifest);
92102

93103
if (!version) {
94104
return { kind: 'not_found', packageName };
95105
}
96106

107+
const githubRepoUrl = manifest ? extractGitHubRepoUrl(manifest) : undefined;
108+
97109
const { packageId, dbVersion } = await ctx.runMutation(
98110
internal.packages.ensureExists,
99111
{
100112
name: packageName,
101113
version,
102114
ecosystem: 'npm',
115+
githubRepoUrl,
103116
},
104117
);
105118

@@ -124,6 +137,11 @@ async function trackPackage(
124137
kind: 'updated',
125138
packageName,
126139
version,
140+
versionText: await resolveSlackVersionText(
141+
version,
142+
manifest,
143+
githubRepoUrl,
144+
),
127145
filterLabel: formatMinUpdateType(minUpdateType),
128146
channelName: existing.channelName,
129147
};
@@ -133,6 +151,11 @@ async function trackPackage(
133151
kind: 'already',
134152
packageName,
135153
version,
154+
versionText: await resolveSlackVersionText(
155+
version,
156+
manifest,
157+
githubRepoUrl,
158+
),
136159
filterLabel: formatMinUpdateType(existing.minUpdateType as MinUpdateType),
137160
channelName: existing.channelName,
138161
};
@@ -148,12 +171,24 @@ async function trackPackage(
148171
userId: channelId ? undefined : userId,
149172
});
150173

174+
const displayVersion = pendingUpdate ? dbVersion : version;
175+
151176
return {
152177
kind: 'tracked',
153178
packageId,
154179
packageName,
155180
version,
156-
displayVersion: pendingUpdate ? dbVersion : version,
181+
displayVersion,
182+
versionText: await resolveSlackVersionText(
183+
version,
184+
manifest,
185+
githubRepoUrl,
186+
),
187+
displayVersionText: await resolveSlackVersionText(
188+
displayVersion,
189+
manifest,
190+
githubRepoUrl,
191+
),
157192
filterLabel: formatMinUpdateType(minUpdateType),
158193
pendingUpdate,
159194
};
@@ -162,11 +197,11 @@ async function trackPackage(
162197
function formatTrackOutcomeLine(outcome: TrackOutcome): string {
163198
switch (outcome.kind) {
164199
case 'tracked':
165-
return `• ${formatSlackPackageLink(outcome.packageName)} — current version ${formatVersion(outcome.displayVersion)}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}${outcome.pendingUpdate ? ` (update available: ${formatVersion(outcome.version)})` : ''}`;
200+
return `• ${formatSlackPackageLink(outcome.packageName)} — current version ${outcome.displayVersionText}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}${outcome.pendingUpdate ? ` (update available: ${outcome.versionText})` : ''}`;
166201
case 'updated':
167-
return `• ${formatSlackPackageLink(outcome.packageName)} — updated threshold to ${outcome.filterLabel ?? 'all'} notifications, current version ${formatVersion(outcome.version)}`;
202+
return `• ${formatSlackPackageLink(outcome.packageName)} — updated threshold to ${outcome.filterLabel ?? 'all'} notifications, current version ${outcome.versionText}`;
168203
case 'already':
169-
return `• ${formatSlackPackageLink(outcome.packageName)} — already tracked at ${formatVersion(outcome.version)}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}`;
204+
return `• ${formatSlackPackageLink(outcome.packageName)} — already tracked at ${outcome.versionText}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}`;
170205
case 'not_found':
171206
return `• ${formatPackageName(outcome.packageName)} — not found on npm`;
172207
}
@@ -593,7 +628,7 @@ export const processNpmTrack = internalAction({
593628
const channelLabel = formatChannelPhrase(outcome.channelName);
594629
await sendFeedback(
595630
details,
596-
`Updated: now tracking ${formatSlackPackageLink(packageName)}${channelLabel} with ${outcome.filterLabel ?? 'all'} notifications — currently at ${formatVersion(outcome.version)}`,
631+
`Updated: now tracking ${formatSlackPackageLink(packageName)}${channelLabel} with ${outcome.filterLabel ?? 'all'} notifications — currently at ${outcome.versionText}`,
597632
);
598633
if (!responseUrl && userId) {
599634
await ctx.scheduler.runAfter(
@@ -609,7 +644,7 @@ export const processNpmTrack = internalAction({
609644
const channelLabel = formatChannelPhrase(outcome.channelName);
610645
await sendFeedback(
611646
details,
612-
`Already tracking ${formatSlackPackageLink(packageName)} — currently at ${formatVersion(outcome.version)}${channelLabel}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}`,
647+
`Already tracking ${formatSlackPackageLink(packageName)} — currently at ${outcome.versionText}${channelLabel}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}`,
613648
);
614649
if (!responseUrl && userId) {
615650
await ctx.scheduler.runAfter(
@@ -623,7 +658,7 @@ export const processNpmTrack = internalAction({
623658

624659
if (details) {
625660
const updateSuffix = outcome.pendingUpdate
626-
? ` There's already an update available (${formatVersion(outcome.displayVersion)}${formatVersion(outcome.version)}) — I'll notify you about it shortly.`
661+
? ` There's already an update available (${outcome.displayVersionText}${outcome.versionText}) — I'll notify you about it shortly.`
627662
: '';
628663

629664
if (channelId) {
@@ -632,7 +667,7 @@ export const processNpmTrack = internalAction({
632667
await chatPostMessage(
633668
details.accessToken,
634669
channelId,
635-
`<@${userId}> is now tracking ${formatSlackPackageLink(packageName)} in this channel — current version ${formatVersion(outcome.displayVersion)}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}${updateSuffix}`,
670+
`<@${userId}> is now tracking ${formatSlackPackageLink(packageName)} in this channel — current version ${outcome.displayVersionText}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}${updateSuffix}`,
636671
);
637672
} catch (error) {
638673
if (error instanceof PrivateChannelError) {
@@ -654,7 +689,7 @@ export const processNpmTrack = internalAction({
654689
await chatPostMessage(
655690
details.accessToken,
656691
userId,
657-
`You're now tracking ${formatSlackPackageLink(packageName)} — current version ${formatVersion(outcome.displayVersion)}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}.${updateSuffix || " I'll DM you when updates are available."}`,
692+
`You're now tracking ${formatSlackPackageLink(packageName)} — current version ${outcome.displayVersionText}${outcome.filterLabel ? ` ${outcome.filterLabel}` : ''}.${updateSuffix || " I'll DM you when updates are available."}`,
658693
);
659694
}
660695
}

0 commit comments

Comments
 (0)