@@ -185,15 +185,63 @@ function extractJobIdFromMessage(text: string): string | null {
185185}
186186
187187/**
188- * Parse a GitHub PR URL into its components.
189- * Returns null if the URL is not a valid PR URL.
188+ * Parse a GitHub PR URL into its owner, repo, and PR number components.
189+ *
190+ * Accepts URLs like: https://github.com/owner/repo/pull/123
190191 */
191- function parsePrUrl ( url : string ) : { repoUrl : string ; prNumber : number } | null {
192- const match = url . match ( / ^ h t t p s ? : \/ \/ g i t h u b \. c o m \/ ( [ ^ / ] + \/ [ ^ / ] + ) \/ p u l l \/ ( \d + ) / )
192+ function parsePrUrl ( prUrl : string ) : { owner : string ; repo : string ; number : number } | null {
193+ const match = prUrl . match ( / g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ p u l l \/ ( \d + ) / )
193194 if ( ! match ) return null
194- return {
195- repoUrl : `https://github.com/${ match [ 1 ] } ` ,
196- prNumber : parseInt ( match [ 2 ] , 10 ) ,
195+ return { owner : match [ 1 ] , repo : match [ 2 ] , number : parseInt ( match [ 3 ] , 10 ) }
196+ }
197+
198+ /**
199+ * Merge a pull request via the GitHub REST API.
200+ */
201+ async function mergePullRequest ( owner : string , repo : string , prNumber : number ) : Promise < void > {
202+ const res = await fetch (
203+ `https://api.github.com/repos/${ owner } /${ repo } /pulls/${ prNumber } /merge` ,
204+ {
205+ method : 'PUT' ,
206+ headers : {
207+ Authorization : `Bearer ${ GITHUB_TOKEN } ` ,
208+ Accept : 'application/vnd.github+json' ,
209+ 'X-GitHub-Api-Version' : '2022-11-28' ,
210+ } ,
211+ body : JSON . stringify ( {
212+ merge_method : 'squash' ,
213+ } ) ,
214+ } ,
215+ )
216+
217+ if ( ! res . ok ) {
218+ const body = await res . text ( )
219+ throw new Error ( `GitHub merge failed (${ res . status } ): ${ body } ` )
220+ }
221+ }
222+
223+ /**
224+ * Close a pull request via the GitHub REST API (without merging).
225+ */
226+ async function closePullRequest ( owner : string , repo : string , prNumber : number ) : Promise < void > {
227+ const res = await fetch (
228+ `https://api.github.com/repos/${ owner } /${ repo } /pulls/${ prNumber } ` ,
229+ {
230+ method : 'PATCH' ,
231+ headers : {
232+ Authorization : `Bearer ${ GITHUB_TOKEN } ` ,
233+ Accept : 'application/vnd.github+json' ,
234+ 'X-GitHub-Api-Version' : '2022-11-28' ,
235+ } ,
236+ body : JSON . stringify ( {
237+ state : 'closed' ,
238+ } ) ,
239+ } ,
240+ )
241+
242+ if ( ! res . ok ) {
243+ const body = await res . text ( )
244+ throw new Error ( `GitHub close failed (${ res . status } ): ${ body } ` )
197245 }
198246}
199247
@@ -352,7 +400,7 @@ bot.command('task', async (ctx: Context) => {
352400 }
353401
354402 const ack = await ctx . reply (
355- `\u{1F504} Detected PR #${ prInfo . prNumber } . Queuing revision...` ,
403+ `\u{1F504} Detected PR #${ prInfo . number } . Queuing revision...` ,
356404 { parse_mode : 'HTML' } ,
357405 )
358406
@@ -364,10 +412,10 @@ bot.command('task', async (ctx: Context) => {
364412 }
365413 if ( GITHUB_TOKEN ) ghEnv . GH_TOKEN = GITHUB_TOKEN
366414
367- const nwo = prInfo . repoUrl . replace ( 'https://github.com/' , '' )
415+ const nwo = ` ${ prInfo . owner } / ${ prInfo . repo } `
368416 const headRef = execFileSync (
369417 'gh' ,
370- [ 'api' , `repos/${ nwo } /pulls/${ prInfo . prNumber } ` , '--jq' , '.head.ref' ] ,
418+ [ 'api' , `repos/${ nwo } /pulls/${ prInfo . number } ` , '--jq' , '.head.ref' ] ,
371419 { encoding : 'utf-8' , env : ghEnv } ,
372420 ) . trim ( )
373421
@@ -384,7 +432,7 @@ bot.command('task', async (ctx: Context) => {
384432 const parentJobId = parentJobs ?. [ 0 ] ?. id
385433
386434 const job = await insertJob ( {
387- repoUrl : prInfo . repoUrl ,
435+ repoUrl : `https://github.com/ ${ prInfo . owner } / ${ prInfo . repo } ` ,
388436 task : description ,
389437 chatId : ctx . chat ! . id ,
390438 messageId : ack . message_id ,
@@ -398,7 +446,7 @@ bot.command('task', async (ctx: Context) => {
398446 '\u{1F504} <b>PR revision queued!</b>' ,
399447 '' ,
400448 `<b>Job ID:</b> <code>${ job . id } </code>` ,
401- `<b>PR:</b> #${ prInfo . prNumber } ` ,
449+ `<b>PR:</b> #${ prInfo . number } ` ,
402450 `<b>Branch:</b> <code>${ headRef } </code>` ,
403451 `<b>Feedback:</b> ${ escapeHtml ( description ) } ` ,
404452 '' ,
@@ -652,17 +700,24 @@ bot.callbackQuery(/^approve:(.+)$/, async (ctx) => {
652700 return
653701 }
654702
655- // In a full implementation this would call the GitHub API to merge.
656- // For now, acknowledge the action and provide the PR link.
657- await ctx . answerCallbackQuery ( { text : 'Approval noted!' } )
703+ const parsed = parsePrUrl ( job . pr_url )
704+ if ( ! parsed ) {
705+ await ctx . answerCallbackQuery ( { text : 'Could not parse PR URL.' } )
706+ return
707+ }
708+
709+ await ctx . answerCallbackQuery ( { text : 'Merging PR...' } )
710+
711+ await mergePullRequest ( parsed . owner , parsed . repo , parsed . number )
712+
658713 await ctx . editMessageText (
659714 [
660- `\u{2705} <b>PR approved for merge </b>` ,
715+ `\u{2705} <b>PR merged successfully </b>` ,
661716 '' ,
662717 `<b>Job:</b> <code>${ job . id . slice ( 0 , 8 ) } </code>` ,
663718 `<b>PR:</b> ${ job . pr_url } ` ,
664719 '' ,
665- 'The PR merge has been initiated.' ,
720+ `Merged <code> ${ parsed . owner } / ${ parsed . repo } # ${ parsed . number } </code> via squash merge.` ,
666721 ] . join ( '\n' ) ,
667722 { parse_mode : 'HTML' } ,
668723 )
@@ -685,16 +740,24 @@ bot.callbackQuery(/^reject:(.+)$/, async (ctx) => {
685740 return
686741 }
687742
688- // In a full implementation this would call the GitHub API to close the PR.
689- await ctx . answerCallbackQuery ( { text : 'PR rejected.' } )
743+ const parsed = parsePrUrl ( job . pr_url )
744+ if ( ! parsed ) {
745+ await ctx . answerCallbackQuery ( { text : 'Could not parse PR URL.' } )
746+ return
747+ }
748+
749+ await ctx . answerCallbackQuery ( { text : 'Closing PR...' } )
750+
751+ await closePullRequest ( parsed . owner , parsed . repo , parsed . number )
752+
690753 await ctx . editMessageText (
691754 [
692755 `\u{274C} <b>PR rejected and closed</b>` ,
693756 '' ,
694757 `<b>Job:</b> <code>${ job . id . slice ( 0 , 8 ) } </code>` ,
695758 `<b>PR:</b> ${ job . pr_url } ` ,
696759 '' ,
697- 'The PR has been closed without merging.' ,
760+ `Closed <code> ${ parsed . owner } / ${ parsed . repo } # ${ parsed . number } </code> without merging.` ,
698761 ] . join ( '\n' ) ,
699762 { parse_mode : 'HTML' } ,
700763 )
0 commit comments