1+ #!/usr/bin/env bun --watch
12import { db } from "@/src/db" ;
23import { TaskMetaCollection } from "@/src/db/TaskMeta" ;
34import { gh } from "@/src/gh" ;
@@ -13,23 +14,46 @@ import { z } from "zod";
1314import { createTimeLogger } from "./createTimeLogger" ;
1415import { ghDesignDefaultConfig } from "./default-config" ;
1516import { slackMessageUrlParse , slackMessageUrlStringify } from "./slackMessageUrlParse" ;
17+ import { upsertSlackMessage } from "../gh-desktop-release-notification/upsertSlackMessage" ;
18+ import { ghPaged } from "@/src/paged" ;
1619const tlog = createTimeLogger ( ) ;
17- // Task bot to scan for [Design] labels on PRs and issues and send notifications to product channel
20+
21+ /**
22+ * Github Design Task
23+ * -----------------------
24+ * Task bot to scan for [Design] labels on PRs and issues and send notifications to product channel
25+ * 1. scan specified repos for issues/PRs with [Design] label
26+ * 2. send Slack notification to #product channel
27+ * 3. request review from specified reviewers for PRs
28+ * 4. track open/closed/merged/approved status
29+ * 5. store processed items in database to avoid duplicates
30+ */
31+
32+ // 1. scan these repos
33+ const REPOURLS = [
34+ 'https://github.com/Comfy-Org/ComfyUI_frontend' ,
35+ 'https://github.com/Comfy-Org/desktop' ,
36+ ] ;
37+
38+ // 2. match these labels
39+ const MATCH_LABELS = [ 'Design' ] ;
40+
41+ // 3.1 request review from these users
42+ const REQUEST_REVIEWERS = [ "PabloWiedemann" ] ;
43+
44+ // 3.2 notify to this slack channel
45+ const CHANNEL_NAME = 'product-design'
46+ const SLACK_MESSAGE_TEMPLATE = `🎨 *New Design {{ITEM_TYPE}}*: {{STATE}} <{{URL}}|{{TITLE}}> {{COMMENTS}} by {{GITHUBUSER}}` ;
1847
1948// Schema for GithubDesignTaskMeta validation
2049export const githubDesignTaskMetaSchema = z . object ( {
2150 name : z . string ( ) . optional ( ) ,
2251 description : z . string ( ) . optional ( ) ,
2352
24- // task config
25- slackChannelName : z . string ( ) . optional ( ) ,
26- slackMessageTemplate : z . string ( ) . optional ( ) ,
27- repoUrls : z . array ( z . string ( ) . url ( ) ) . optional ( ) ,
28- requestReviewers : z . array ( z . string ( ) ) . optional ( ) ,
29- matchLabels : z . string ( ) . optional ( ) ,
53+ // task config, able to update via site web page
54+ // slackMessageTemplate: z.string().optional(),
3055
3156 // cache
32- slackChannelId : z . string ( ) . optional ( ) ,
3357 lastRunAt : z . date ( ) . optional ( ) ,
3458 lastStatus : z . enum ( [ "success" , "error" , "running" ] ) . optional ( ) ,
3559 lastError : z . string ( ) . optional ( ) ,
@@ -107,93 +131,64 @@ export async function runGithubDesignTask() {
107131 description :
108132 "Task to scan for [Design] labeled issues and PRs in specified repositories and notify product channel" ,
109133 // Set defaults if not already set
110- slackChannelId : "" ,
111134 //
112135 lastRunAt : new Date ( ) ,
113136 lastStatus : "running" ,
114137 lastError : "" ,
115138 } ) ;
116139
117- // save default values if not set
118- if ( ! meta . slackMessageTemplate )
119- meta = await GithubDesignTaskMeta . $upsert ( { slackMessageTemplate : ghDesignDefaultConfig . SLACK_MESSAGE_TEMPLATE } ) ;
120- if ( ! meta . requestReviewers )
121- meta = await GithubDesignTaskMeta . $upsert ( { requestReviewers : ghDesignDefaultConfig . REQUEST_REVIEWERS } ) ;
122- if ( ! meta . repoUrls ) meta = await GithubDesignTaskMeta . $upsert ( { repoUrls : ghDesignDefaultConfig . REPOS_TO_SCAN_URLS } ) ;
123- if ( ! meta . matchLabels ) meta = await GithubDesignTaskMeta . $upsert ( { matchLabels : ghDesignDefaultConfig . MATCH_LABEL } ) ;
124- if ( ! meta . slackChannelName )
125- meta = await GithubDesignTaskMeta . $upsert ( { slackChannelName : ghDesignDefaultConfig . SLACK_CHANNEL_NAME } ) ;
126- if ( ! meta . slackChannelId ) {
127- tlog ( "Fetching Slack product channel..." ) ;
128- meta = await GithubDesignTaskMeta . $upsert ( {
129- slackChannelId : ( await getSlackChannel ( meta . slackChannelName ! ) ) . id ,
130- } ) ;
131- }
132-
133140 tlog ( "TaskMeta: " + JSON . stringify ( meta ) ) ;
134-
135- const channel = meta . slackChannelId || DIE ( "Missing Slack channel ID" ) ;
136- tlog ( `Slack channel: ${ meta . slackChannelName } (${ channel } )` ) ;
141+ tlog ( `Slack channel: ${ CHANNEL_NAME } ` ) ;
137142
138143 // Get configuration from meta or use defaults
139- const slackMessageTemplate = meta . slackMessageTemplate || DIE ( "Missing Slack message template" ) ;
140- const designItemsFlow = await sflow ( meta . repoUrls || DIE ( "Missing repo URLs" ) )
141- . map ( ( e ) => parseGithubRepoUrl ( e ) )
142- . pMap (
143- async ( { owner, repo } ) =>
144- pageFlow ( 1 , async ( page ) => {
145- const per_page = 100 ;
146- // will list both issues and PRs
147- const { data : issues } = await gh . issues . listForRepo ( {
148- owner,
149- repo,
150- page,
151- per_page,
152- labels : meta . matchLabels || DIE ( "missing match label" ) , // comma-separated list of labels
153- state : "open" , // scan only opened issues/PRs
154- } ) ;
155- tlog ( `Found ${ issues . length } Design labeled items in ${ owner } /${ repo } ` ) ;
156- return { data : issues , next : issues . length >= per_page ? page + 1 : undefined } ;
157- } )
158- . filter ( ( e ) => e . length )
159- . flat ( )
160- . map ( ( item ) => ( {
161- url : item . html_url ,
162- title : item . title ,
163- body : item . body ,
164- user : item . user ?. login ,
165- type : item . pull_request ? ( "pull_request" as const ) : ( "issue" as const ) ,
166- state : item . pull_request ?. merged_at ? ( "merged" as const ) : ( item . state as "open" | "closed" ) ,
167- stateAt : item . pull_request ?. merged_at || item . closed_at || item . created_at ,
168- labels : item . labels . flatMap ( ( e ) => ( typeof e === "string" ? [ ] : [ e ] ) ) . map ( ( l ) => l . name ) ,
169- comments : item . comments ,
170- } ) ) ,
171- { concurrency : 3 } ,
172- ) // concurrency 3 repos
173- . confluenceByConcat ( ) // merge page flows
174- . map ( async function process ( item ) {
144+ // const slackMessageTemplate = meta.slackMessageTemplate || DIE("Missing Slack message template");
145+ // console.log("Using Slack message template:", JSON.stringify(slackMessageTemplate));
146+
147+ // Start processing design items
148+ const designItemsFlow = await sflow ( REPOURLS )
149+ . map ( ( url ) =>
150+ ghPaged ( gh . issues . listForRepo ) ( {
151+ ...parseGithubRepoUrl ( url ) ,
152+ labels : MATCH_LABELS . join ( ',' ) , // comma-separated list of labels
153+ state : "open" , // scan only opened issues/PRs
154+ } )
155+ )
156+ . confluenceByParallel ( ) // merge page flows
157+ // simplify issue items
158+ . map ( ( issue ) => ( {
159+ url : issue . html_url ,
160+ title : issue . title ,
161+ body : issue . body ,
162+ user : issue . user ?. login ,
163+ type : issue . pull_request ? ( "pull_request" as const ) : ( "issue" as const ) ,
164+ state : issue . pull_request ?. merged_at ? ( "merged" as const ) : ( issue . state as "open" | "closed" ) ,
165+ stateAt : issue . pull_request ?. merged_at || issue . closed_at || issue . created_at ,
166+ labels : issue . labels . flatMap ( ( e ) => ( typeof e === "string" ? [ ] : [ e ] ) ) . map ( ( l ) => l . name ) ,
167+ comments : issue . comments ,
168+ } ) )
169+ . map ( async function processIssueItems ( issueInfo ) {
175170 tlog (
176- `PROCESSING ${ item . url } #${ item . title . replace ( / \s + / g, "+" ) } _ ${ item . body ?. slice ( 0 , 20 ) . replaceAll ( / \s + / g, "+" ) } ` ,
171+ `PROCESSING ${ issueInfo . url } #${ issueInfo . title . replace ( / \s + / g, "+" ) } ${ issueInfo . body ?. slice ( 0 , 20 ) . replaceAll ( / \s + / g, "+" ) } ` ,
177172 ) ;
178- const url = item . url ;
173+ const url = issueInfo . url ;
179174 const { owner, repo, issue_number } = parseIssueUrl ( url ) ;
180175 // create task
181176 let task = await saveGithubDesignTask ( url , {
182- type : item . type ,
183- state : item . state ,
184- stateAt : new Date ( item . stateAt ) ,
185- title : item . title ,
186- user : item . user || "?" ,
187- comments : item . comments || 0 ,
188- bodyHash : item . body ? sha256 ( item . body ) : undefined ,
177+ type : issueInfo . type , // issue or pull_request
178+ state : issueInfo . state ,
179+ stateAt : new Date ( issueInfo . stateAt ) ,
180+ title : issueInfo . title ,
181+ user : issueInfo . user || "?" ,
182+ comments : issueInfo . comments || 0 ,
183+ bodyHash : issueInfo . body ? sha256 ( issueInfo . body ) : undefined ,
189184 lastRunAt : new Date ( ) ,
190185 taskStatus : "pending" ,
191186 lastDoneAt : null , // reset lastDoneAt
192187 } ) ;
193188
194189 if ( task . state === "open" ) {
195- if ( task . type === "pull_request" && meta . requestReviewers ? .some ( ( e ) => ! task . reviewers ?. includes ( e ) ) ) {
196- const requestReviewers = meta . requestReviewers ?? DIE ( "Missing request reviewers" ) ;
190+ if ( task . type === "pull_request" && REQUEST_REVIEWERS . some ( ( e ) => ! task . reviewers ?. includes ( e ) ) ) {
191+ const requestReviewers = REQUEST_REVIEWERS ;
197192 const newReviewers = requestReviewers . filter ( ( e ) => ! task . reviewers ?. includes ( e ) ) ;
198193 tlog ( `Requesting reviewers: ${ newReviewers . join ( ", " ) } ` ) ;
199194 if ( ! dryRun ) {
@@ -207,7 +202,8 @@ export async function runGithubDesignTask() {
207202 }
208203 }
209204
210- const text = ( meta . slackMessageTemplate || DIE ( "Missing Slack message template" ) )
205+ const text = SLACK_MESSAGE_TEMPLATE
206+ // (meta.slackMessageTemplate || DIE("Missing Slack message template"))
211207 . replace ( "{{COMMENTS}}" , task . comments ?. toString ( ) . replace ( / ^ ( .* ) $ / , "[r$1]" ) ?? "" )
212208 . replace ( "{{STATE}}" , task . state . toUpperCase ( ) )
213209 . replace ( "{{USERNAME}}" , task . user ?? "=??=" )
@@ -221,11 +217,7 @@ export async function runGithubDesignTask() {
221217 if ( ! task . slackUrl ) {
222218 tlog ( `Sending Slack Notification for design task: ${ task . url } (${ task . type } )` ) ;
223219 if ( ! dryRun ) {
224- const slack = getSlack ( ) ;
225- const msg = await slack . chat . postMessage ( {
226- channel,
227- text,
228- } ) ;
220+ const msg = await upsertSlackMessage ( { channelName : CHANNEL_NAME , text, } ) ;
229221 if ( ! msg . ok ) {
230222 await saveGithubDesignTask ( url , {
231223 error : `Failed to send Slack message: ${ msg . error } ` ,
@@ -234,21 +226,19 @@ export async function runGithubDesignTask() {
234226 throw new Error ( `Failed to send Slack message: ${ msg . error } ` ) ;
235227 }
236228 task = await saveGithubDesignTask ( url , {
237- slackUrl : slackMessageUrlStringify ( { channel, ts : msg . ts ! } ) ,
229+ slackUrl : slackMessageUrlStringify ( { channel : msg . channel , ts : msg . ts ! } ) ,
238230 slackSentAt : new Date ( ) ,
239231 slackMsgHash,
240232 } ) ;
241233 tlog ( `Slack message sent: ${ task . slackUrl } ` ) ;
242234 }
243235 } else {
244- // update slack message if outdated
236+ // update slack message if its outdated
245237 if ( task . slackMsgHash !== slackMsgHash ) {
246238 tlog ( `Updating Slack message for task: ${ task . url } ` ) ;
247239 if ( ! dryRun ) {
248- const slack = getSlack ( ) ;
249- await slack . chat . update ( {
250- ...slackMessageUrlParse ( task . slackUrl ) ,
251- text,
240+ await upsertSlackMessage ( {
241+ ...slackMessageUrlParse ( task . slackUrl ) , text,
252242 } ) ;
253243 task = await saveGithubDesignTask ( url , { slackMsgHash } ) ;
254244 tlog ( `Slack message updated: ${ task . slackUrl } ` ) ;
0 commit comments