Skip to content

Commit 63b3f50

Browse files
snomiaoclaudeCopilot
authored
feat: add ComfyUI_frontend release notification task (#67)
* feat: add ComfyUI_frontend release notification task Created a new GitHub release notification task for monitoring the ComfyUI_frontend repository. This task monitors releases and sends notifications to Slack for stable, prerelease, and draft releases. - Added gh-frontend-release-notification task based on the desktop release notification pattern - Integrated with the run-gh-tasks scheduler - Added comprehensive test coverage with mocked dependencies - Configured to send notifications to the #frontend Slack channel 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Update CLAUDE.md Co-authored-by: Copilot <[email protected]> * fix: correct Slack message sending logic to handle undefined text fields - Fixed logic that was always evaluating to true when comparing undefined text fields - Separated condition checks for better clarity and correctness - Added proper handling for message updates with url parameter - Enhanced test coverage for edge cases including duplicate message prevention * fix: correct Slack message sending logic to handle undefined text fields - Add proper checks for undefined text fields in Slack messages - Ensure message sending works correctly with or without text content - Improve error handling for frontend release notifications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 5fc0c62 commit 63b3f50

File tree

4 files changed

+495
-0
lines changed

4 files changed

+495
-0
lines changed

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,16 @@ const stats = await getGhCacheStats();
102102

103103
- Short args: `gh.repos.get({"owner":"octocat","repo":"Hello-World"})#b3117af2`
104104
- Long args: `gh.repos.get({"owner":"octocat","descripti...bbbbbbbbbb"})#4240f076`
105+
106+
## SFlow Stream Processing Library
107+
108+
### Overview
109+
110+
SFlow is a powerful functional stream processing library used throughout the codebase for handling asynchronous data operations. It provides a rich set of utilities for transforming streams with a functional programming approach, similar to RxJS but optimized for modern JavaScript/TypeScript and WebStreams.
111+
112+
### Implementation Details
113+
114+
- **Package**: `[email protected]`
115+
- **Author**: snomiao
116+
- **License**: MIT
117+
- **Core Concepts**: SFlow is built around composable stream operators, lazy evaluation, and support for both synchronous and asynchronous data flows.
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
import { db } from "@/src/db";
2+
import { gh } from "@/src/gh";
3+
import { parseGithubRepoUrl } from "@/src/parseOwnerRepo";
4+
import { getSlackChannel } from "@/src/slack/channels";
5+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
6+
import runGithubFrontendReleaseNotificationTask from "./index";
7+
8+
jest.mock("@/src/gh");
9+
jest.mock("@/src/slack/channels");
10+
jest.mock("../gh-desktop-release-notification/upsertSlackMessage");
11+
12+
const mockGh = gh as jest.Mocked<typeof gh>;
13+
const mockGetSlackChannel = getSlackChannel as jest.MockedFunction<typeof getSlackChannel>;
14+
const { upsertSlackMessage } = jest.requireMock("../gh-desktop-release-notification/upsertSlackMessage");
15+
16+
describe("GithubFrontendReleaseNotificationTask", () => {
17+
let collection: any;
18+
19+
beforeEach(async () => {
20+
jest.clearAllMocks();
21+
22+
collection = {
23+
findOne: jest.fn(),
24+
findOneAndUpdate: jest.fn(),
25+
createIndex: jest.fn(),
26+
};
27+
28+
jest.spyOn(db, "collection").mockReturnValue(collection);
29+
30+
mockGetSlackChannel.mockResolvedValue({
31+
id: "test-channel-id",
32+
name: "frontend",
33+
} as any);
34+
35+
upsertSlackMessage.mockResolvedValue({
36+
text: "mocked message",
37+
channel: "test-channel-id",
38+
url: "https://slack.com/message/123",
39+
});
40+
});
41+
42+
afterEach(async () => {
43+
jest.restoreAllMocks();
44+
});
45+
46+
describe("parseGithubRepoUrl", () => {
47+
it("should correctly parse ComfyUI_frontend repo URL", () => {
48+
const result = parseGithubRepoUrl("https://github.com/Comfy-Org/ComfyUI_frontend");
49+
expect(result).toEqual({
50+
owner: "Comfy-Org",
51+
repo: "ComfyUI_frontend",
52+
});
53+
});
54+
});
55+
56+
describe("Release Processing", () => {
57+
it("should process stable releases and send message only on first occurrence", async () => {
58+
const mockRelease = {
59+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0",
60+
tag_name: "v1.0.0",
61+
draft: false,
62+
prerelease: false,
63+
created_at: new Date().toISOString(),
64+
published_at: new Date().toISOString(),
65+
body: "Release notes",
66+
};
67+
68+
mockGh.repos = {
69+
listReleases: jest.fn().mockResolvedValue({
70+
data: [mockRelease],
71+
}),
72+
} as any;
73+
74+
// First call - no existing message
75+
collection.findOneAndUpdate.mockResolvedValueOnce({
76+
url: mockRelease.html_url,
77+
version: mockRelease.tag_name,
78+
status: "stable",
79+
isStable: true,
80+
createdAt: new Date(mockRelease.created_at),
81+
releasedAt: new Date(mockRelease.published_at),
82+
});
83+
84+
// Second call - save with message
85+
collection.findOneAndUpdate.mockResolvedValueOnce({
86+
url: mockRelease.html_url,
87+
version: mockRelease.tag_name,
88+
status: "stable",
89+
isStable: true,
90+
createdAt: new Date(mockRelease.created_at),
91+
releasedAt: new Date(mockRelease.published_at),
92+
slackMessage: {
93+
text: "🎨 ComfyUI_frontend <https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0|Release v1.0.0> is stable!",
94+
channel: "test-channel-id",
95+
url: "https://slack.com/message/123",
96+
},
97+
});
98+
99+
await runGithubFrontendReleaseNotificationTask();
100+
101+
expect(mockGh.repos.listReleases).toHaveBeenCalledWith({
102+
owner: "Comfy-Org",
103+
repo: "ComfyUI_frontend",
104+
per_page: 3,
105+
});
106+
107+
expect(upsertSlackMessage).toHaveBeenCalledWith(
108+
expect.objectContaining({
109+
channel: "test-channel-id",
110+
text: expect.stringContaining("stable"),
111+
}),
112+
);
113+
});
114+
115+
it("should not send duplicate messages for unchanged releases", async () => {
116+
const mockRelease = {
117+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0",
118+
tag_name: "v1.0.0",
119+
draft: false,
120+
prerelease: false,
121+
created_at: new Date().toISOString(),
122+
published_at: new Date().toISOString(),
123+
body: "Release notes",
124+
};
125+
126+
mockGh.repos = {
127+
listReleases: jest.fn().mockResolvedValue({
128+
data: [mockRelease],
129+
}),
130+
} as any;
131+
132+
// Return task with existing message text matching new message
133+
collection.findOneAndUpdate.mockResolvedValue({
134+
url: mockRelease.html_url,
135+
version: mockRelease.tag_name,
136+
status: "stable",
137+
isStable: true,
138+
createdAt: new Date(mockRelease.created_at),
139+
releasedAt: new Date(mockRelease.published_at),
140+
slackMessage: {
141+
text: "🎨 ComfyUI_frontend <https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0|Release v1.0.0> is stable!",
142+
channel: "test-channel-id",
143+
url: "https://slack.com/message/123",
144+
},
145+
});
146+
147+
await runGithubFrontendReleaseNotificationTask();
148+
149+
// Should not call upsertSlackMessage since text hasn't changed
150+
expect(upsertSlackMessage).not.toHaveBeenCalled();
151+
});
152+
153+
it("should process prerelease and send drafting message", async () => {
154+
const mockPrerelease = {
155+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0-beta.1",
156+
tag_name: "v1.0.0-beta.1",
157+
draft: false,
158+
prerelease: true,
159+
created_at: new Date().toISOString(),
160+
published_at: new Date().toISOString(),
161+
body: "Beta release notes",
162+
};
163+
164+
mockGh.repos = {
165+
listReleases: jest.fn().mockResolvedValue({
166+
data: [mockPrerelease],
167+
}),
168+
} as any;
169+
170+
// First call - save initial data
171+
collection.findOneAndUpdate.mockResolvedValueOnce({
172+
url: mockPrerelease.html_url,
173+
version: mockPrerelease.tag_name,
174+
status: "prerelease",
175+
isStable: false,
176+
createdAt: new Date(mockPrerelease.created_at),
177+
releasedAt: new Date(mockPrerelease.published_at),
178+
});
179+
180+
// Second call - save with drafting message
181+
collection.findOneAndUpdate.mockResolvedValueOnce({
182+
url: mockPrerelease.html_url,
183+
version: mockPrerelease.tag_name,
184+
status: "prerelease",
185+
isStable: false,
186+
createdAt: new Date(mockPrerelease.created_at),
187+
releasedAt: new Date(mockPrerelease.published_at),
188+
slackMessageDrafting: {
189+
text: "🎨 ComfyUI_frontend <https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0-beta.1|Release v1.0.0-beta.1> is prerelease!",
190+
channel: "test-channel-id",
191+
url: "https://slack.com/message/456",
192+
},
193+
});
194+
195+
await runGithubFrontendReleaseNotificationTask();
196+
197+
expect(upsertSlackMessage).toHaveBeenCalledWith(
198+
expect.objectContaining({
199+
channel: "test-channel-id",
200+
text: expect.stringContaining("prerelease"),
201+
}),
202+
);
203+
});
204+
205+
it("should process draft releases", async () => {
206+
const mockDraft = {
207+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v2.0.0",
208+
tag_name: "v2.0.0",
209+
draft: true,
210+
prerelease: false,
211+
created_at: new Date().toISOString(),
212+
published_at: null,
213+
body: "Draft release notes",
214+
};
215+
216+
mockGh.repos = {
217+
listReleases: jest.fn().mockResolvedValue({
218+
data: [mockDraft],
219+
}),
220+
} as any;
221+
222+
collection.findOneAndUpdate.mockResolvedValue({
223+
url: mockDraft.html_url,
224+
version: mockDraft.tag_name,
225+
status: "draft",
226+
isStable: false,
227+
createdAt: new Date(mockDraft.created_at),
228+
releasedAt: undefined,
229+
});
230+
231+
await runGithubFrontendReleaseNotificationTask();
232+
233+
expect(collection.findOneAndUpdate).toHaveBeenCalledWith(
234+
{ url: mockDraft.html_url },
235+
expect.objectContaining({
236+
$set: expect.objectContaining({
237+
status: "draft",
238+
isStable: false,
239+
releasedAt: undefined,
240+
}),
241+
}),
242+
{ upsert: true, returnDocument: "after" },
243+
);
244+
});
245+
246+
it("should skip old releases before sendSince date", async () => {
247+
const oldRelease = {
248+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v0.1.0",
249+
tag_name: "v0.1.0",
250+
draft: false,
251+
prerelease: false,
252+
created_at: "2024-01-01T00:00:00Z",
253+
published_at: "2024-01-01T00:00:00Z",
254+
body: "Old release",
255+
};
256+
257+
mockGh.repos = {
258+
listReleases: jest.fn().mockResolvedValue({
259+
data: [oldRelease],
260+
}),
261+
} as any;
262+
263+
collection.findOneAndUpdate.mockResolvedValue({
264+
url: oldRelease.html_url,
265+
version: oldRelease.tag_name,
266+
status: "stable",
267+
isStable: true,
268+
createdAt: new Date(oldRelease.created_at),
269+
releasedAt: new Date(oldRelease.published_at),
270+
});
271+
272+
await runGithubFrontendReleaseNotificationTask();
273+
274+
// Should save the release but not send a message
275+
expect(collection.findOneAndUpdate).toHaveBeenCalledTimes(1);
276+
expect(upsertSlackMessage).not.toHaveBeenCalled();
277+
});
278+
279+
it("should update message when release text changes", async () => {
280+
const mockRelease = {
281+
html_url: "https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0",
282+
tag_name: "v1.0.1", // Changed version
283+
draft: false,
284+
prerelease: false,
285+
created_at: new Date().toISOString(),
286+
published_at: new Date().toISOString(),
287+
body: "Updated release notes",
288+
};
289+
290+
mockGh.repos = {
291+
listReleases: jest.fn().mockResolvedValue({
292+
data: [mockRelease],
293+
}),
294+
} as any;
295+
296+
// Return task with old message text
297+
collection.findOneAndUpdate.mockResolvedValueOnce({
298+
url: mockRelease.html_url,
299+
version: mockRelease.tag_name,
300+
status: "stable",
301+
isStable: true,
302+
createdAt: new Date(mockRelease.created_at),
303+
releasedAt: new Date(mockRelease.published_at),
304+
slackMessage: {
305+
text: "🎨 ComfyUI_frontend <https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0|Release v1.0.0> is stable!",
306+
channel: "test-channel-id",
307+
url: "https://slack.com/message/123",
308+
},
309+
});
310+
311+
// Second call after update
312+
collection.findOneAndUpdate.mockResolvedValueOnce({
313+
url: mockRelease.html_url,
314+
version: mockRelease.tag_name,
315+
status: "stable",
316+
isStable: true,
317+
slackMessage: {
318+
text: "🎨 ComfyUI_frontend <https://github.com/Comfy-Org/ComfyUI_frontend/releases/tag/v1.0.0|Release v1.0.1> is stable!",
319+
channel: "test-channel-id",
320+
url: "https://slack.com/message/123",
321+
},
322+
});
323+
324+
await runGithubFrontendReleaseNotificationTask();
325+
326+
// Should update the message since text changed
327+
expect(upsertSlackMessage).toHaveBeenCalledWith(
328+
expect.objectContaining({
329+
url: "https://slack.com/message/123",
330+
text: expect.stringContaining("v1.0.1"),
331+
}),
332+
);
333+
});
334+
});
335+
336+
describe("Database Index", () => {
337+
it("should create unique index on url field", async () => {
338+
expect(collection.createIndex).toHaveBeenCalledWith({ url: 1 }, { unique: true });
339+
});
340+
});
341+
});

0 commit comments

Comments
 (0)