comfy-pr 0.2.26 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -157
- package/bot/IdleWaiter.ts +31 -0
- package/bot/RestartManager.spec.ts +163 -0
- package/bot/RestartManager.ts +190 -0
- package/bot/WorkingTasksManager.spec.ts +154 -0
- package/bot/cli.ts +1171 -0
- package/bot/codesearch-ai-wip.ts +351 -0
- package/bot/error-collector.ts +133 -0
- package/bot/index.ts +14 -0
- package/bot/restart-example.ts +99 -0
- package/bot/slack-bolt.ts +688 -0
- package/bot/slack-bot.ts +1575 -0
- package/bot/state.ts +19 -0
- package/bot/templateLoader.test.ts +84 -0
- package/bot/templateLoader.ts +92 -0
- package/next.config.ts +26 -0
- package/package.json +161 -41
- package/post-summary.ts +44 -0
- package/src/Authors.ts +2 -2
- package/src/CMNodes.ts +3 -3
- package/src/CNRepos.ts +30 -7
- package/src/CRNodes.ts +11 -9
- package/src/CRPulls.ts +3 -0
- package/src/EmailTasks.ts +27 -9
- package/src/FORK_OWNER.ts +3 -1
- package/src/FollowRules.ts +3 -3
- package/src/GithubIssueComments.ts +1 -1
- package/src/Totals.ts +9 -8
- package/src/WorkerInstances.ts +31 -8
- package/src/addCommentAction.ts +19 -11
- package/src/analyzePullsStatus.ts +39 -17
- package/src/analyzeTotals.ts +19 -11
- package/src/bypassRepos.ts +6 -5
- package/src/checkPRsFailures.ts +13 -8
- package/src/cli.test.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
- package/src/createComfyRegistryPullRequests.ts +35 -10
- package/src/createGithubForkForRepo.ts +36 -11
- package/src/createGithubPullRequest.ts +83 -33
- package/src/createIssueComment.ts +4 -4
- package/src/fetchComfyRegistryNodes.ts +34 -4
- package/src/fetchCurrentGeoInfo.ts +3 -1
- package/src/fetchRelatedPullWithComments.ts +1 -1
- package/src/fetchRepoDescriptionMap.ts +1 -4
- package/src/followRuleSchema.ts +10 -4
- package/src/getBranchWorkingDir.ts +3 -7
- package/src/getRepoWorkingDir.ts +2 -3
- package/src/gh.spec.ts +571 -0
- package/src/ghData.ts +13 -0
- package/src/ghPageFlow.ts +33 -0
- package/src/ghSubscriber.ts +76 -0
- package/src/ghUser.ts +28 -4
- package/src/index.ts +13 -4
- package/src/initializeFollowRules.ts +11 -4
- package/src/logger.spec.ts +95 -0
- package/src/logger.ts +46 -0
- package/src/makeGpl3LicenseBranch.ts +66 -0
- package/src/makePublishBranch.ts +21 -15
- package/src/makeTomlBranch.ts +48 -14
- package/src/makeUpdateTomlLicenseBranch.ts +40 -44
- package/src/matchRelatedPulls.ts +9 -4
- package/src/normalizeGithubUrl.spec.ts +131 -0
- package/src/normalizeGithubUrl.ts +64 -0
- package/src/parseIssueUrl.spec.ts +95 -0
- package/src/parseIssueUrl.ts +17 -3
- package/src/parseOwnerRepo.spec.ts +147 -0
- package/src/parseOwnerRepo.ts +8 -5
- package/src/parsePullsState.ts +2 -2
- package/src/parseTitleBodyOfMarkdown.ts +1 -1
- package/src/pickRepoInfo.ts +37 -0
- package/src/postSlackMessage.ts +5 -1
- package/src/preload.ts +30 -27
- package/src/readTemplateTitle.ts +1 -3
- package/src/sendEmailAction.ts +14 -10
- package/src/sendGmail.ts +3 -3
- package/src/updateAuthorsForGithub.ts +53 -35
- package/src/updateAuthorsFromCNRepo.ts +12 -5
- package/src/updateCMNodesDuplicationWarnings.ts +14 -9
- package/src/updateCMRepos.ts +1 -1
- package/src/updateCNRepos.ts +14 -14
- package/src/updateCNReposCRPullsComments.ts +7 -5
- package/src/updateCNReposInfo.ts +49 -40
- package/src/updateCNReposPRCandidate.ts +3 -3
- package/src/updateCNReposPulls.ts +9 -5
- package/src/updateCNReposPullsDashboard.ts +11 -9
- package/src/updateCNReposRelatedPulls.ts +3 -3
- package/src/updateCRNodes.ts +24 -5
- package/src/updateCRRepos.ts +1 -1
- package/src/updateComfyTotals.ts +7 -5
- package/src/updateFollowRuleSet.ts +17 -9
- package/src/updateOutdatedPullsTemplates.ts +61 -48
- package/src/updateSlackMessages.ts +8 -4
- package/tailwind.config.ts +7 -1
- package/next-env.d.ts +0 -5
- package/src/GIT_USEREMAIL.ts +0 -5
- package/src/GIT_USERNAME.ts +0 -9
- package/src/clone_modify_push_Branches.ts +0 -21
- package/src/tomlFillDescription.ts +0 -17
package/src/gh.spec.ts
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { http, HttpResponse } from "msw";
|
|
2
|
+
import { server } from "./test/msw-setup";
|
|
3
|
+
|
|
4
|
+
// Import the GitHub client
|
|
5
|
+
// Note: We need to mock the environment variable before importing
|
|
6
|
+
process.env.GH_TOKEN = "test-token-for-gh-spec";
|
|
7
|
+
|
|
8
|
+
// Dynamic import to ensure env var is set
|
|
9
|
+
const ghModule = await import("../lib/github/githubCached");
|
|
10
|
+
const { gh } = await import("../lib/github/githubCached");
|
|
11
|
+
const { ghc, clearGhCache } = ghModule;
|
|
12
|
+
|
|
13
|
+
describe("GitHub API Client (gh)", () => {
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Clear cache before each test to ensure clean state
|
|
16
|
+
await clearGhCache();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("Repos API", () => {
|
|
20
|
+
it("should get a repository", async () => {
|
|
21
|
+
const result = await gh.repos.get({
|
|
22
|
+
owner: "octocat",
|
|
23
|
+
repo: "Hello-World",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.data).toBeDefined();
|
|
27
|
+
expect(result.data.name).toBe("Hello-World");
|
|
28
|
+
expect(result.data.owner.login).toBe("octocat");
|
|
29
|
+
expect(result.data.default_branch).toBe("main");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should create a fork", async () => {
|
|
33
|
+
const result = await gh.repos.createFork({
|
|
34
|
+
owner: "octocat",
|
|
35
|
+
repo: "Hello-World",
|
|
36
|
+
organization: "test-org",
|
|
37
|
+
name: "Hello-World-fork",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.data).toBeDefined();
|
|
41
|
+
expect(result.data.fork).toBe(true);
|
|
42
|
+
expect(result.data.owner.login).toBe("test-org");
|
|
43
|
+
expect(result.data.name).toBe("Hello-World-fork");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should get a branch", async () => {
|
|
47
|
+
const result = await gh.repos.getBranch({
|
|
48
|
+
owner: "octocat",
|
|
49
|
+
repo: "Hello-World",
|
|
50
|
+
branch: "main",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.data).toBeDefined();
|
|
54
|
+
expect(result.data.name).toBe("main");
|
|
55
|
+
expect(result.data.commit.sha).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should list tags", async () => {
|
|
59
|
+
const result = await gh.repos.listTags({
|
|
60
|
+
owner: "comfyanonymous",
|
|
61
|
+
repo: "ComfyUI",
|
|
62
|
+
per_page: 10,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.data).toBeDefined();
|
|
66
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
67
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
68
|
+
expect(result.data[0].name).toBeDefined();
|
|
69
|
+
expect(result.data[0].commit.sha).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should get a commit", async () => {
|
|
73
|
+
const result = await gh.repos.getCommit({
|
|
74
|
+
owner: "octocat",
|
|
75
|
+
repo: "Hello-World",
|
|
76
|
+
ref: "abc123def456",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.data).toBeDefined();
|
|
80
|
+
expect(result.data.commit.author!.date).toBeDefined();
|
|
81
|
+
expect(result.data.commit.message).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should list releases", async () => {
|
|
85
|
+
const result = await gh.repos.listReleases({
|
|
86
|
+
owner: "Comfy-Org",
|
|
87
|
+
repo: "desktop",
|
|
88
|
+
per_page: 3,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.data).toBeDefined();
|
|
92
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
93
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
94
|
+
expect(result.data[0].tag_name).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should list webhooks", async () => {
|
|
98
|
+
const result = await gh.repos.listWebhooks({
|
|
99
|
+
owner: "octocat",
|
|
100
|
+
repo: "Hello-World",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.data).toBeDefined();
|
|
104
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should create a webhook", async () => {
|
|
108
|
+
const result = await gh.repos.createWebhook({
|
|
109
|
+
owner: "octocat",
|
|
110
|
+
repo: "Hello-World",
|
|
111
|
+
config: {
|
|
112
|
+
url: "https://example.com/webhook",
|
|
113
|
+
content_type: "json",
|
|
114
|
+
},
|
|
115
|
+
events: ["push", "pull_request"],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.data).toBeDefined();
|
|
119
|
+
expect(result.data.config.url).toBe("https://example.com/webhook");
|
|
120
|
+
expect(result.data.events).toContain("push");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("Pulls API", () => {
|
|
125
|
+
it("should list pull requests", async () => {
|
|
126
|
+
const result = await gh.pulls.list({
|
|
127
|
+
owner: "octocat",
|
|
128
|
+
repo: "Hello-World",
|
|
129
|
+
state: "all",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.data).toBeDefined();
|
|
133
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
134
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should filter pull requests by state", async () => {
|
|
138
|
+
const result = await gh.pulls.list({
|
|
139
|
+
owner: "octocat",
|
|
140
|
+
repo: "Hello-World",
|
|
141
|
+
state: "open",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.data).toBeDefined();
|
|
145
|
+
expect(result.data.every((pr) => pr.state === "open")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should get a specific pull request", async () => {
|
|
149
|
+
const result = await gh.pulls.get({
|
|
150
|
+
owner: "octocat",
|
|
151
|
+
repo: "Hello-World",
|
|
152
|
+
pull_number: 101,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.data).toBeDefined();
|
|
156
|
+
expect(result.data.number).toBe(101);
|
|
157
|
+
expect(result.data.title).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should create a pull request", async () => {
|
|
161
|
+
const result = await gh.pulls.create({
|
|
162
|
+
owner: "octocat",
|
|
163
|
+
repo: "Hello-World",
|
|
164
|
+
title: "Test PR",
|
|
165
|
+
body: "Test PR body",
|
|
166
|
+
head: "test-user:feature-branch",
|
|
167
|
+
base: "main",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result.data).toBeDefined();
|
|
171
|
+
expect(result.data.title).toBe("Test PR");
|
|
172
|
+
expect(result.data.number).toBe(999);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should update a pull request", async () => {
|
|
176
|
+
const result = await gh.pulls.update({
|
|
177
|
+
owner: "octocat",
|
|
178
|
+
repo: "Hello-World",
|
|
179
|
+
pull_number: 101,
|
|
180
|
+
title: "Updated PR Title",
|
|
181
|
+
body: "Updated PR body",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(result.data).toBeDefined();
|
|
185
|
+
expect(result.data.title).toBe("Updated PR Title");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should request reviewers", async () => {
|
|
189
|
+
const result = await gh.pulls.requestReviewers({
|
|
190
|
+
owner: "octocat",
|
|
191
|
+
repo: "Hello-World",
|
|
192
|
+
pull_number: 101,
|
|
193
|
+
reviewers: ["reviewer1", "reviewer2"],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.data).toBeDefined();
|
|
197
|
+
expect(result.data.requested_reviewers).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should list review comments", async () => {
|
|
201
|
+
const result = await gh.pulls.listReviewComments({
|
|
202
|
+
owner: "octocat",
|
|
203
|
+
repo: "Hello-World",
|
|
204
|
+
pull_number: 101,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result.data).toBeDefined();
|
|
208
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("Issues API", () => {
|
|
213
|
+
it("should list issues", async () => {
|
|
214
|
+
const result = await gh.issues.listForRepo({
|
|
215
|
+
owner: "octocat",
|
|
216
|
+
repo: "Hello-World",
|
|
217
|
+
state: "open",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result.data).toBeDefined();
|
|
221
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should filter issues by labels", async () => {
|
|
225
|
+
const result = await gh.issues.listForRepo({
|
|
226
|
+
owner: "octocat",
|
|
227
|
+
repo: "Hello-World",
|
|
228
|
+
labels: "bug",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.data).toBeDefined();
|
|
232
|
+
expect(
|
|
233
|
+
result.data.every((issue) =>
|
|
234
|
+
issue.labels.some((l) => (typeof l === "string" ? l === "bug" : l.name === "bug")),
|
|
235
|
+
),
|
|
236
|
+
).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should get a specific issue", async () => {
|
|
240
|
+
const result = await gh.issues.get({
|
|
241
|
+
owner: "octocat",
|
|
242
|
+
repo: "Hello-World",
|
|
243
|
+
issue_number: 50,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(result.data).toBeDefined();
|
|
247
|
+
expect(result.data.number).toBe(50);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should update an issue", async () => {
|
|
251
|
+
const result = await gh.issues.update({
|
|
252
|
+
owner: "octocat",
|
|
253
|
+
repo: "Hello-World",
|
|
254
|
+
issue_number: 50,
|
|
255
|
+
body: "Updated issue body",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(result.data).toBeDefined();
|
|
259
|
+
expect(result.data.body).toBe("Updated issue body");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should list issue comments", async () => {
|
|
263
|
+
const result = await gh.issues.listComments({
|
|
264
|
+
owner: "octocat",
|
|
265
|
+
repo: "Hello-World",
|
|
266
|
+
issue_number: 50,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.data).toBeDefined();
|
|
270
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should create an issue comment", async () => {
|
|
274
|
+
const result = await gh.issues.createComment({
|
|
275
|
+
owner: "octocat",
|
|
276
|
+
repo: "Hello-World",
|
|
277
|
+
issue_number: 50,
|
|
278
|
+
body: "Test comment",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(result.data).toBeDefined();
|
|
282
|
+
expect(result.data.body).toBe("Test comment");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should update a comment", async () => {
|
|
286
|
+
const result = await gh.issues.updateComment({
|
|
287
|
+
owner: "octocat",
|
|
288
|
+
repo: "Hello-World",
|
|
289
|
+
comment_id: 1,
|
|
290
|
+
body: "Updated comment",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result.data).toBeDefined();
|
|
294
|
+
expect(result.data.body).toBe("Updated comment");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should get a comment", async () => {
|
|
298
|
+
const result = await gh.issues.getComment({
|
|
299
|
+
owner: "octocat",
|
|
300
|
+
repo: "Hello-World",
|
|
301
|
+
comment_id: 1,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(result.data).toBeDefined();
|
|
305
|
+
expect(result.data.body).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should add labels to an issue", async () => {
|
|
309
|
+
const result = await gh.issues.addLabels({
|
|
310
|
+
owner: "octocat",
|
|
311
|
+
repo: "Hello-World",
|
|
312
|
+
issue_number: 50,
|
|
313
|
+
labels: ["bug", "enhancement"],
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.data).toBeDefined();
|
|
317
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should list timeline events", async () => {
|
|
321
|
+
const result = await gh.issues.listEventsForTimeline({
|
|
322
|
+
owner: "octocat",
|
|
323
|
+
repo: "Hello-World",
|
|
324
|
+
issue_number: 50,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.data).toBeDefined();
|
|
328
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("Git API", () => {
|
|
333
|
+
it("should get an annotated tag", async () => {
|
|
334
|
+
const result = await gh.git.getTag({
|
|
335
|
+
owner: "comfyanonymous",
|
|
336
|
+
repo: "ComfyUI",
|
|
337
|
+
tag_sha: "abc123",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(result.data).toBeDefined();
|
|
341
|
+
expect(result.data.tag).toBeDefined();
|
|
342
|
+
expect(result.data.tagger).toBeDefined();
|
|
343
|
+
expect(result.data.message).toBeDefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// TODO: Fix MSW handler override for error responses - currently times out
|
|
347
|
+
// The issue is that server.use() runtime handler override doesn't properly
|
|
348
|
+
// intercept requests when testing error scenarios with Octokit
|
|
349
|
+
it.skip("should handle non-annotated tag errors gracefully", async () => {
|
|
350
|
+
// Mock a 404 response for lightweight tags
|
|
351
|
+
server.use(
|
|
352
|
+
http.get("https://api.github.com/repos/:owner/:repo/git/tags/:tag_sha", () => {
|
|
353
|
+
return new HttpResponse(null, {
|
|
354
|
+
status: 404,
|
|
355
|
+
statusText: "Not Found",
|
|
356
|
+
});
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
await gh.git.getTag({
|
|
362
|
+
owner: "comfyanonymous",
|
|
363
|
+
repo: "ComfyUI",
|
|
364
|
+
tag_sha: "lightweight-tag",
|
|
365
|
+
});
|
|
366
|
+
// Should not reach here
|
|
367
|
+
expect(true).toBe(false);
|
|
368
|
+
} catch (error: unknown) {
|
|
369
|
+
expect((error as { status: number }).status).toBe(404);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Users API", () => {
|
|
375
|
+
it("should get authenticated user", async () => {
|
|
376
|
+
const result = await gh.users.getAuthenticated();
|
|
377
|
+
|
|
378
|
+
expect(result.data).toBeDefined();
|
|
379
|
+
expect(result.data.login).toBe("test-authenticated-user");
|
|
380
|
+
expect(result.data.email).toBeDefined();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should get a user by username", async () => {
|
|
384
|
+
const result = await gh.users.getByUsername({
|
|
385
|
+
username: "octocat",
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(result.data).toBeDefined();
|
|
389
|
+
expect(result.data.login).toBe("octocat");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// TODO: Fix MSW handler override for error responses - currently times out
|
|
394
|
+
// The issue is that server.use() runtime handler override doesn't properly
|
|
395
|
+
// intercept requests when testing error scenarios with Octokit
|
|
396
|
+
describe("Error Handling", () => {
|
|
397
|
+
it.skip("should handle 404 errors", async () => {
|
|
398
|
+
server.use(
|
|
399
|
+
http.get("https://api.github.com/repos/:owner/:repo", () => {
|
|
400
|
+
return new HttpResponse(
|
|
401
|
+
JSON.stringify({
|
|
402
|
+
message: "Not Found",
|
|
403
|
+
documentation_url: "https://docs.github.com/rest",
|
|
404
|
+
}),
|
|
405
|
+
{
|
|
406
|
+
status: 404,
|
|
407
|
+
headers: {
|
|
408
|
+
"Content-Type": "application/json",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
await gh.repos.get({
|
|
417
|
+
owner: "nonexistent",
|
|
418
|
+
repo: "nonexistent",
|
|
419
|
+
});
|
|
420
|
+
// Should not reach here
|
|
421
|
+
expect(true).toBe(false);
|
|
422
|
+
} catch (error: unknown) {
|
|
423
|
+
expect((error as { status: number }).status).toBe(404);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it.skip("should handle rate limit errors", async () => {
|
|
428
|
+
server.use(
|
|
429
|
+
http.get("https://api.github.com/repos/:owner/:repo", () => {
|
|
430
|
+
return new HttpResponse(
|
|
431
|
+
JSON.stringify({
|
|
432
|
+
message: "API rate limit exceeded",
|
|
433
|
+
documentation_url:
|
|
434
|
+
"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting",
|
|
435
|
+
}),
|
|
436
|
+
{
|
|
437
|
+
status: 403,
|
|
438
|
+
headers: {
|
|
439
|
+
"Content-Type": "application/json",
|
|
440
|
+
"X-RateLimit-Limit": "60",
|
|
441
|
+
"X-RateLimit-Remaining": "0",
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
await gh.repos.get({
|
|
450
|
+
owner: "octocat",
|
|
451
|
+
repo: "Hello-World",
|
|
452
|
+
});
|
|
453
|
+
// Should not reach here
|
|
454
|
+
expect(true).toBe(false);
|
|
455
|
+
} catch (error: unknown) {
|
|
456
|
+
expect((error as { status: number }).status).toBe(403);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("GitHub Cached Client (ghc)", () => {
|
|
463
|
+
beforeEach(async () => {
|
|
464
|
+
await clearGhCache();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("should cache API responses", async () => {
|
|
468
|
+
// First call - should hit the API
|
|
469
|
+
const result1 = await ghc.repos.get({
|
|
470
|
+
owner: "octocat",
|
|
471
|
+
repo: "Hello-World",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(result1.data).toBeDefined();
|
|
475
|
+
expect(result1.data.name).toBe("Hello-World");
|
|
476
|
+
|
|
477
|
+
// Modify the mock to return different data
|
|
478
|
+
server.use(
|
|
479
|
+
http.get("https://api.github.com/repos/:owner/:repo", () => {
|
|
480
|
+
return HttpResponse.json({
|
|
481
|
+
id: 999999,
|
|
482
|
+
name: "Modified-Repo",
|
|
483
|
+
full_name: "octocat/Modified-Repo",
|
|
484
|
+
owner: {
|
|
485
|
+
login: "octocat",
|
|
486
|
+
id: 1,
|
|
487
|
+
},
|
|
488
|
+
default_branch: "main",
|
|
489
|
+
});
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Second call - should use cache and return original data
|
|
494
|
+
const result2 = await ghc.repos.get({
|
|
495
|
+
owner: "octocat",
|
|
496
|
+
repo: "Hello-World",
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Should still have the original cached data
|
|
500
|
+
expect(result2.data.name).toBe("Hello-World");
|
|
501
|
+
expect(result2.data.id).toBe(123456);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("should cache different parameters separately", async () => {
|
|
505
|
+
const result1 = await ghc.repos.get({
|
|
506
|
+
owner: "octocat",
|
|
507
|
+
repo: "Hello-World",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const result2 = await ghc.repos.get({
|
|
511
|
+
owner: "octocat",
|
|
512
|
+
repo: "Spoon-Knife",
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(result1.data.name).toBe("Hello-World");
|
|
516
|
+
expect(result2.data.name).toBe("Spoon-Knife");
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("should clear cache when requested", async () => {
|
|
520
|
+
// First call - populate cache
|
|
521
|
+
const result1 = await ghc.repos.get({
|
|
522
|
+
owner: "octocat",
|
|
523
|
+
repo: "Hello-World",
|
|
524
|
+
});
|
|
525
|
+
expect(result1.data.name).toBe("Hello-World");
|
|
526
|
+
|
|
527
|
+
// Modify the mock
|
|
528
|
+
server.use(
|
|
529
|
+
http.get("https://api.github.com/repos/:owner/:repo", () => {
|
|
530
|
+
return HttpResponse.json({
|
|
531
|
+
id: 999999,
|
|
532
|
+
name: "Modified-Repo",
|
|
533
|
+
full_name: "octocat/Modified-Repo",
|
|
534
|
+
owner: {
|
|
535
|
+
login: "octocat",
|
|
536
|
+
id: 1,
|
|
537
|
+
},
|
|
538
|
+
default_branch: "main",
|
|
539
|
+
});
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Clear cache
|
|
544
|
+
await clearGhCache();
|
|
545
|
+
|
|
546
|
+
// Third call - should hit the API and get new data
|
|
547
|
+
const result3 = await ghc.repos.get({
|
|
548
|
+
owner: "octocat",
|
|
549
|
+
repo: "Hello-World",
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(result3.data.name).toBe("Modified-Repo");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("should handle concurrent requests efficiently", async () => {
|
|
556
|
+
const promises = Array(10)
|
|
557
|
+
.fill(null)
|
|
558
|
+
.map(() =>
|
|
559
|
+
ghc.repos.get({
|
|
560
|
+
owner: "octocat",
|
|
561
|
+
repo: "Hello-World",
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const results = await Promise.all(promises);
|
|
566
|
+
|
|
567
|
+
// All results should be identical
|
|
568
|
+
expect(results.every((r) => r.data.name === "Hello-World")).toBe(true);
|
|
569
|
+
expect(results.every((r) => r.data.id === 123456)).toBe(true);
|
|
570
|
+
});
|
|
571
|
+
});
|
package/src/ghData.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to fetch GitHub data using a provided fetch endpoint.
|
|
3
|
+
*
|
|
4
|
+
* @param fetchEndpoint - A function that fetches data from GitHub API.
|
|
5
|
+
* @returns A function that takes parameters and returns the fetched data.
|
|
6
|
+
*
|
|
7
|
+
* @deprecated use .then(e=>e.data) instead, because this function adds little value
|
|
8
|
+
*/
|
|
9
|
+
export function ghData<Params extends object & { page?: number; per_page?: number }, Item>(
|
|
10
|
+
fetchEndpoint: (params?: Params) => Promise<{ data: Item }>,
|
|
11
|
+
): (params: Params) => Promise<Item> {
|
|
12
|
+
return (params) => fetchEndpoint(params).then((res) => res.data);
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type sflow from "sflow";
|
|
2
|
+
import { pageFlow } from "sflow";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper to paginate through GitHub API endpoints that support pagination.
|
|
6
|
+
*
|
|
7
|
+
* @param listEndpoint The GitHub API endpoint function that returns a paginated list.
|
|
8
|
+
* @returns A function that takes the same parameters as the endpoint (excluding pagination parameters)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const flow = ghPaged(ghc.pulls.listCommentsForReview)({ owner: "snomiao", repo: "ComfyNode-Registry-test", pull_number: 1, review_id: 1 })
|
|
12
|
+
* const allComments = flow.toArray()
|
|
13
|
+
*
|
|
14
|
+
* @see https://docs.github.com/en/rest/guides/traversing-with-pagination
|
|
15
|
+
*
|
|
16
|
+
* @template Params The type of parameters accepted by the GitHub API endpoint.
|
|
17
|
+
* @template Item The type of items returned by the GitHub API endpoint.
|
|
18
|
+
*
|
|
19
|
+
* @author snomiao <snomiao@gmail.com>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export function ghPageFlow<Params extends object & { page?: number; per_page?: number }, Item>(
|
|
23
|
+
listEndpoint: (params?: Params) => Promise<{ data: Item[] }>,
|
|
24
|
+
// options
|
|
25
|
+
{ per_page = 100, startPage = 1 } = {},
|
|
26
|
+
): (params: Params & { page?: never; per_page?: never }) => sflow<Item> {
|
|
27
|
+
return (params) =>
|
|
28
|
+
pageFlow(startPage, async (page) => {
|
|
29
|
+
const { data } = await listEndpoint({ ...params, page, per_page });
|
|
30
|
+
|
|
31
|
+
return { data, next: data.length >= per_page ? page + 1 : null };
|
|
32
|
+
}).flat();
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sflow from "sflow";
|
|
2
|
+
import { createOctokit } from "@/lib/github/createOctokit";
|
|
3
|
+
import { parseGithubRepoUrl } from "./parseOwnerRepo";
|
|
4
|
+
|
|
5
|
+
const tokens = process.env.GH_SUBSCRIBER_TOKENS?.split(",") || [];
|
|
6
|
+
const gh = (token: string) => createOctokit({ auth: token }).rest;
|
|
7
|
+
|
|
8
|
+
// dont await
|
|
9
|
+
showSubscriberUsers();
|
|
10
|
+
|
|
11
|
+
if (import.meta.main) {
|
|
12
|
+
await showSubscriberUsers();
|
|
13
|
+
// subscribeToRepo('')
|
|
14
|
+
await watchRepo("https://github.com/snomiao/ComfyNode-Registry-test");
|
|
15
|
+
// await listNotifications()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function showSubscriberUsers() {
|
|
19
|
+
console.log(
|
|
20
|
+
`GH_TOKEN_SUBSCRIBER Users: `,
|
|
21
|
+
await sflow(tokens)
|
|
22
|
+
.map((token) => gh(token))
|
|
23
|
+
.map(async (gh) => {
|
|
24
|
+
const { data: user } = await gh.users.getAuthenticated();
|
|
25
|
+
return `${user.login} <${user.email}>`;
|
|
26
|
+
})
|
|
27
|
+
.join(", ")
|
|
28
|
+
.text(),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
async function _listNotifications() {
|
|
32
|
+
console.log(
|
|
33
|
+
`Messages: `,
|
|
34
|
+
await sflow(tokens)
|
|
35
|
+
.map((token) => gh(token))
|
|
36
|
+
.map(async (gh) => {
|
|
37
|
+
const { data: result } = await gh.activity.listNotificationsForAuthenticatedUser();
|
|
38
|
+
return JSON.stringify(result);
|
|
39
|
+
})
|
|
40
|
+
.toArray(),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// list subscribed threads
|
|
45
|
+
// await sflow(tokens)
|
|
46
|
+
// .map((token) => gh(token))
|
|
47
|
+
// .map(async (gh) => {
|
|
48
|
+
// console.log(await gh.activity.getRepoSubscription({owner: }));
|
|
49
|
+
// })
|
|
50
|
+
// .run();
|
|
51
|
+
|
|
52
|
+
export async function watchRepo(repoUrl: string) {
|
|
53
|
+
await sflow(tokens)
|
|
54
|
+
.map((token) => gh(token))
|
|
55
|
+
.map(async (gh) => {
|
|
56
|
+
const result = await gh.activity.setRepoSubscription({
|
|
57
|
+
...parseGithubRepoUrl(repoUrl),
|
|
58
|
+
subscribed: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(result);
|
|
62
|
+
})
|
|
63
|
+
.run();
|
|
64
|
+
}
|
|
65
|
+
export async function unwatchRepo(repoUrl: string) {
|
|
66
|
+
await sflow(tokens)
|
|
67
|
+
.map((token) => gh(token))
|
|
68
|
+
.map(async (gh) => {
|
|
69
|
+
const result = await gh.activity.setRepoSubscription({
|
|
70
|
+
...parseGithubRepoUrl(repoUrl),
|
|
71
|
+
subscribed: false,
|
|
72
|
+
});
|
|
73
|
+
console.log(result);
|
|
74
|
+
})
|
|
75
|
+
.run();
|
|
76
|
+
}
|