Skip to content

Commit 58a11e9

Browse files
authored
Merge pull request #56 from Comfy-Org/sno-coreping
feat(workflows): add CorePing GitHub workflow for Core PR review reminders
2 parents 7849416 + 7785d04 commit 58a11e9

File tree

8 files changed

+224
-115
lines changed

8 files changed

+224
-115
lines changed

.github/workflows/coreping.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: "CorePing - Core PR Review Reminder"
2+
3+
on:
4+
schedule:
5+
# Runs at 11am San Francisco time (19:00 UTC), Monday to Saturday
6+
# 0 19 * * 1-6 means: minute=0, hour=19 (7pm UTC), any day of month, any month, Mon-Sat (1-6)
7+
- cron: "0 19 * * 1-6"
8+
workflow_dispatch:
9+
# push:
10+
# branches:
11+
# - main
12+
# paths:
13+
# - ".github/workflows/coreping.yaml"
14+
# - "app/tasks/coreping/**"
15+
16+
jobs:
17+
run_coreping:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 10
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: oven-sh/setup-bun@v1
23+
24+
# Install dependencies
25+
- run: bun i
26+
27+
# Run CorePing task
28+
- run: bun app/tasks/coreping/index.tsx
29+
timeout-minutes: 8
30+
env:
31+
GH_TOKEN_COMFY_PR: ${{ secrets.GH_TOKEN_COMFY_PR_BOT }}
32+
MONGODB_URI: ${{ secrets.MONGODB_URI }}
33+
SLACK_BOT_CHANNEL: ${{ secrets.SLACK_BOT_CHANNEL }}
34+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

app/api/router.ts

Lines changed: 98 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const router = t.router({
2121
.meta({ openapi: { method: "GET", path: "/version", description: "Get version of ComfyPR" } })
2222
.input(z.object({}))
2323
.output(z.object({ version: z.string() }))
24-
.query(({ }) => ({ version: pkg.version })),
24+
.query(({}) => ({ version: pkg.version })),
2525
dumpCsv: t.procedure
2626
.meta({ openapi: { method: "GET", path: "/dump.csv", description: "Get csv dump" } })
2727
.input(z.object({}))
@@ -40,20 +40,22 @@ export const router = t.router({
4040
analyzePullsStatus: t.procedure
4141
.meta({ openapi: { method: "GET", path: "/analyze-pulls-status", description: "Get current worker" } })
4242
.input(z.object({ skip: z.number(), limit: z.number() }).partial())
43-
.output(z.object({
44-
updated: z.string(), // deprecated
45-
pull_updated: z.string(),
46-
repo_updated: z.string(),
47-
on_registry: z.boolean(),
48-
state: z.enum(["OPEN", "MERGED", "CLOSED"]),
49-
url: z.string(),
50-
head: z.string(),
51-
comments: z.number(),
52-
lastcomment: z.string(),
53-
ownername: z.string().optional(),
54-
repository: z.string().optional(),
55-
author_email: z.string().optional(),
56-
}))
43+
.output(
44+
z.object({
45+
updated: z.string(), // deprecated
46+
pull_updated: z.string(),
47+
repo_updated: z.string(),
48+
on_registry: z.boolean(),
49+
state: z.enum(["OPEN", "MERGED", "CLOSED"]),
50+
url: z.string(),
51+
head: z.string(),
52+
comments: z.number(),
53+
lastcomment: z.string(),
54+
ownername: z.string().optional(),
55+
repository: z.string().optional(),
56+
author_email: z.string().optional(),
57+
}),
58+
)
5759
.query(async ({ input: { limit = 0, skip = 0 } }) => (await analyzePullsStatus({ limit, skip })) as any),
5860
getRepoUrls: t.procedure
5961
.meta({ openapi: { method: "GET", path: "/repo-urls", description: "Get repo urls" } })
@@ -100,15 +102,19 @@ export const router = t.router({
100102
openapi: { method: "GET", path: "/github-contributor-analyze", description: "Get github contributor analyze" },
101103
})
102104
.input(z.object({ url: z.string() }))
103-
.output(z.object({
104-
repoUrl: z.string(),
105-
contributors: z.array(z.object({
106-
count: z.number(),
107-
name: z.string(),
108-
email: z.string(),
109-
})),
110-
updatedAt: z.date(),
111-
}))
105+
.output(
106+
z.object({
107+
repoUrl: z.string(),
108+
contributors: z.array(
109+
z.object({
110+
count: z.number(),
111+
name: z.string(),
112+
email: z.string(),
113+
}),
114+
),
115+
updatedAt: z.date(),
116+
}),
117+
)
112118
.query(async ({ input: { url } }) => {
113119
// await import { githubContributorAnalyze } from "../tasks/github-contributor-analyze/githubContributorAnalyze";
114120
const { githubContributorAnalyze } = await import("../tasks/github-contributor-analyze/githubContributorAnalyze");
@@ -121,21 +127,25 @@ export const router = t.router({
121127
openapi: { method: "GET", path: "/tasks/gh-design/meta", description: "Get github design task metadata" },
122128
})
123129
.input(z.object({}))
124-
.output(z.object({
125-
meta: z.object({
126-
name: z.string().optional(),
127-
description: z.string().optional(),
128-
slackChannelName: z.string().optional(),
129-
slackMessageTemplate: z.string().optional(),
130-
repoUrls: z.array(z.string()).optional(),
131-
requestReviewers: z.array(z.string()).optional(),
132-
matchLabels: z.string().optional(),
133-
slackChannelId: z.string().optional(),
134-
lastRunAt: z.date().optional(),
135-
lastStatus: z.enum(["success", "error", "running"]).optional(),
136-
lastError: z.string().optional(),
137-
}).nullable()
138-
}))
130+
.output(
131+
z.object({
132+
meta: z
133+
.object({
134+
name: z.string().optional(),
135+
description: z.string().optional(),
136+
slackChannelName: z.string().optional(),
137+
slackMessageTemplate: z.string().optional(),
138+
repoUrls: z.array(z.string()).optional(),
139+
requestReviewers: z.array(z.string()).optional(),
140+
matchLabels: z.string().optional(),
141+
slackChannelId: z.string().optional(),
142+
lastRunAt: z.date().optional(),
143+
lastStatus: z.enum(["success", "error", "running"]).optional(),
144+
lastError: z.string().optional(),
145+
})
146+
.nullable(),
147+
}),
148+
)
139149
.query(async () => {
140150
try {
141151
const meta = await GithubDesignTaskMeta.findOne({ coll: "GithubDesignTask" });
@@ -150,58 +160,65 @@ export const router = t.router({
150160
.meta({
151161
openapi: { method: "PATCH", path: "/tasks/gh-design/meta", description: "Update github design task metadata" },
152162
})
153-
.input(z.object({
154-
slackMessageTemplate: z.string()
155-
.min(1, "Slack message template cannot be empty")
156-
.refine(
157-
(template) => template.includes("{{ITEM_TYPE}}"),
158-
"Slack message template must include {{ITEM_TYPE}} placeholder"
159-
)
160-
.refine(
161-
(template) => template.includes("{{URL}}"),
162-
"Slack message template must include {{URL}} placeholder"
163-
)
164-
.refine(
165-
(template) => template.includes("{{TITLE}}"),
166-
"Slack message template must include {{TITLE}} placeholder"
167-
)
168-
.optional(),
169-
requestReviewers: z.array(z.string().min(1, "Reviewer username cannot be empty")).optional(),
170-
repoUrls: z.array(
171-
z.string()
172-
.url("Repository URL must be a valid URL")
163+
.input(
164+
z.object({
165+
slackMessageTemplate: z
166+
.string()
167+
.min(1, "Slack message template cannot be empty")
173168
.refine(
174-
(url) => url.startsWith("https://github.com"),
175-
"Repository URL must start with https://github.com"
169+
(template) => template.includes("{{ITEM_TYPE}}"),
170+
"Slack message template must include {{ITEM_TYPE}} placeholder",
176171
)
177-
).optional(),
178-
}))
179-
.output(z.object({
180-
success: z.boolean(),
181-
meta: z.object({
182-
name: z.string().optional(),
183-
description: z.string().optional(),
184-
slackChannelName: z.string().optional(),
185-
slackMessageTemplate: z.string().optional(),
186-
repoUrls: z.array(z.string()).optional(),
187-
requestReviewers: z.array(z.string()).optional(),
188-
matchLabels: z.string().optional(),
189-
slackChannelId: z.string().optional(),
190-
lastRunAt: z.date().optional(),
191-
lastStatus: z.enum(["success", "error", "running"]).optional(),
192-
lastError: z.string().optional(),
193-
}).nullable()
194-
}))
172+
.refine((template) => template.includes("{{URL}}"), "Slack message template must include {{URL}} placeholder")
173+
.refine(
174+
(template) => template.includes("{{TITLE}}"),
175+
"Slack message template must include {{TITLE}} placeholder",
176+
)
177+
.optional(),
178+
requestReviewers: z.array(z.string().min(1, "Reviewer username cannot be empty")).optional(),
179+
repoUrls: z
180+
.array(
181+
z
182+
.string()
183+
.url("Repository URL must be a valid URL")
184+
.refine(
185+
(url) => url.startsWith("https://github.com"),
186+
"Repository URL must start with https://github.com",
187+
),
188+
)
189+
.optional(),
190+
}),
191+
)
192+
.output(
193+
z.object({
194+
success: z.boolean(),
195+
meta: z
196+
.object({
197+
name: z.string().optional(),
198+
description: z.string().optional(),
199+
slackChannelName: z.string().optional(),
200+
slackMessageTemplate: z.string().optional(),
201+
repoUrls: z.array(z.string()).optional(),
202+
requestReviewers: z.array(z.string()).optional(),
203+
matchLabels: z.string().optional(),
204+
slackChannelId: z.string().optional(),
205+
lastRunAt: z.date().optional(),
206+
lastStatus: z.enum(["success", "error", "running"]).optional(),
207+
lastError: z.string().optional(),
208+
})
209+
.nullable(),
210+
}),
211+
)
195212
.mutation(async ({ input }) => {
196-
throw new Error("Meta editing functionality is temporarily disabled. This feature is under maintenance.");
213+
throw new Error("Meta editing functionality is temporarily disabled. This feature is under maintenance.");
197214
// TODO: add back later
198215
try {
199216
const updateData: any = {};
200217
if (input.slackMessageTemplate !== undefined) updateData.slackMessageTemplate = input.slackMessageTemplate;
201218
if (input.requestReviewers !== undefined) updateData.requestReviewers = input.requestReviewers;
202219
if (input.repoUrls !== undefined) updateData.repoUrls = input.repoUrls;
203220

204-
const meta = await GithubDesignTaskMeta.$set(updateData);
221+
const meta = await GithubDesignTaskMeta.$upsert(updateData);
205222
return { success: true, meta };
206223
} catch (error) {
207224
console.error("Failed to update metadata:", error);

app/tasks/coreping/index.tsx

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { tsmatch } from "@/packages/mongodb-pipeline-ts/Task";
44
import { db } from "@/src/db";
5+
import { TaskMetaCollection } from "@/src/db/TaskMeta";
56
import type { GH } from "@/src/gh";
67
import { ghc } from "@/src/ghc";
78
import { parseIssueUrl } from "@/src/parseIssueUrl";
@@ -10,6 +11,8 @@ import DIE from "@snomiao/die";
1011
import chalk from "chalk";
1112
import sflow, { pageFlow } from "sflow";
1213
import { P } from "ts-pattern";
14+
import z from "zod";
15+
import { upsertSlackMessage } from "../gh-desktop-release-notification/upsertSlackMessage";
1316
/**
1417
* [Comfy- CorePing] The Core/Important PR Review Reminder Service
1518
* This service reminders @comfyanonymous for unreviewed Core/Core-Important PRs every 24 hours in the morning 8am of california
@@ -41,6 +44,9 @@ export const coreReviewTrackerConfig = {
4144
],
4245
labels: ["Core", "CoreImportant"],
4346
minReminderInterval: "24h", // edit-existed-slack-message < this-interval < send-another-slack-message
47+
slackChannelName: "develop", // develop channel, without notification
48+
49+
// github message
4450
messageUpdatePattern: "<!-- COMFY_PR_BOT_TRACKER -->",
4551
staleMessage: `<!-- COMFY_PR_BOT_TRACKER --> This PR has been waiting for a response for too long. A reminder is being sent to @comfyanonymous.`,
4652
reviewedMessage: `<!-- COMFY_PR_BOT_TRACKER --> This PR is reviewed! When it's ready for review again, please add a comment with **+label:Core-Ready-for-Review** to reminder @comfyanonymous to restart the review process.`,
@@ -71,6 +77,26 @@ type ComfyCorePRs = {
7177
task_updated_at: Date;
7278
};
7379
export const ComfyCorePRs = db.collection<ComfyCorePRs>("ComfyCorePRs");
80+
81+
/* only one */
82+
// type ComfyCorePRsLastMessage = {
83+
// url: string;
84+
// text: string;
85+
// };
86+
// export const ComfyCorePRsLastMessage = db.collection<ComfyCorePRsLastMessage>("ComfyCorePRsLastMessage");
87+
const Meta = TaskMetaCollection(
88+
"ComfyCorePRs",
89+
z.object({
90+
lastSlackMessage: z
91+
.object({
92+
url: z.string(),
93+
text: z.string(),
94+
sendAt: z.date(),
95+
})
96+
.optional(),
97+
}),
98+
);
99+
74100
const saveTask = async (pr: Partial<ComfyCorePRs> & { url: string }) => {
75101
return (
76102
(await ComfyCorePRs.findOneAndUpdate(
@@ -82,6 +108,9 @@ const saveTask = async (pr: Partial<ComfyCorePRs> & { url: string }) => {
82108
};
83109

84110
if (import.meta.main) {
111+
// Designed to be mon to sat, TIME CHECKING
112+
// Pacific Daylight Time
113+
85114
console.log("start", import.meta.file);
86115
let freshCount = 0;
87116

@@ -171,7 +200,8 @@ if (import.meta.main) {
171200
const status = isReviewed ? "reviewed" : isCommented ? "reviewed" : isFresh ? "fresh" : "stale";
172201

173202
const hours = Math.floor(diff / (60 * 60 * 1000));
174-
const statusMsg = `${corePrLabel.name} PR <${pr.html_url}|${pr.title.replace(/\W+/g, " ").trim()}> has been labeled for more than ${hours} hours.`;
203+
const sanitizedTitle = pr.title.replace(/\W+/g, " ").trim();
204+
const statusMsg = `@${pr.user?.login}'s ${corePrLabel.name} PR <${pr.html_url}|${sanitizedTitle}> is waiting for your feedback for more than ${hours} hours.`;
175205
console.log(statusMsg);
176206
console.log(pr.html_url + " " + pr.labels.map((e) => e.name));
177207

@@ -197,7 +227,7 @@ if (import.meta.main) {
197227
})
198228
.run();
199229

200-
const corePRs = await ComfyCorePRs.find({}).sort({ last_labeled_at: -1 }).toArray();
230+
const corePRs = await ComfyCorePRs.find({}).sort({ last_labeled_at: 1 }).toArray();
201231

202232
console.log("ready to send slack message to notify @comfy");
203233
const staleCorePRs = corePRs.filter((pr) => pr.status === "stale");
@@ -206,13 +236,39 @@ if (import.meta.main) {
206236
.join("\n");
207237
const freshCorePRs = corePRs.filter((pr) => pr.status === "fresh");
208238

209-
const notifyMessage = `Hey <@comfy>, Here's x${staleCorePRs.length} Core/Important PRs waiting your feedback!\n\n${staleCorePRsMessage}\n... and there are ${freshCorePRs.length} more fresh Core/Core-Important PRs.\n cc <@Yoland> <@snomiao>`;
239+
const freshMsg = !freshCorePRs.length
240+
? ""
241+
: `and there are ${freshCorePRs.length} more fresh Core/Core-Important PRs.\n`;
242+
const notifyMessage = `Hey <@comfy>, Here's x${staleCorePRs.length} Core/Important PRs waiting your feedback!\n\n${staleCorePRsMessage}\n${freshMsg}\nSent from <CorePing> by <@snomiao> cc <@Yoland>`;
210243
console.log(chalk.bgBlue(notifyMessage));
211244

212245
// TODO: update message with delete line when it's reviewed
246+
// send or update slack message
247+
let meta = await Meta.$upsert({});
248+
// if <24 h since last sent (not edit), update that msg
249+
if (
250+
meta.lastSlackMessage &&
251+
meta.lastSlackMessage.sendAt &&
252+
new Date().getTime() - new Date(meta.lastSlackMessage.sendAt).getTime() < 23.9 * 60 * 60 * 1000
253+
) {
254+
const msg = await upsertSlackMessage({
255+
text: notifyMessage,
256+
channelName: cfg.slackChannelName,
257+
url: meta.lastSlackMessage.url,
258+
});
259+
meta = await Meta.$upsert({ lastSlackMessage: { text: msg.text, url: msg.url, sendAt: new Date() } });
260+
} else {
261+
// if >24h or not exist, post a new msg
262+
const msg = await upsertSlackMessage({
263+
text: notifyMessage,
264+
channelName: cfg.slackChannelName,
265+
});
266+
meta = await Meta.$upsert({ lastSlackMessage: { text: msg.text, url: msg.url, sendAt: new Date() } });
267+
}
213268

214269
console.log("done", import.meta.file);
215270
}
271+
216272
/**
217273
* get full timeline
218274
* - [Issue event types - GitHub Docs]( https://docs.github.com/en/rest/using-the-rest-api/issue-event-types?apiVersion=2022-11-28 )

0 commit comments

Comments
 (0)