@@ -23,6 +23,8 @@ import (
23
23
"io"
24
24
"net/http"
25
25
"os"
26
+ "regexp"
27
+ "strconv"
26
28
"strings"
27
29
28
30
"github.com/google/go-github/v75/github"
@@ -52,6 +54,7 @@ Auto-generated report for ${jobName} job build.
52
54
53
55
Link to failed build: ${linkToBuild}
54
56
Commit: ${commit}
57
+ PR: ${prNumber}
55
58
56
59
### Component(s)
57
60
${component}
@@ -66,6 +69,10 @@ Link to latest failed build: ${linkToBuild}
66
69
Commit: ${commit}
67
70
68
71
${failedTests}
72
+ `
73
+ prCommentTemplate = `@${prAuthor} some tests are failing on main after these changes.
74
+ Details: ${issueLink}
75
+ Please take a look when you get a chance. Thanks!
69
76
`
70
77
)
71
78
@@ -201,7 +208,11 @@ func (c *Client) GetExistingIssue(ctx context.Context, module string) *github.Is
201
208
// information about the latest failure. This method is expected to be
202
209
// called only if there's an existing open Issue for the current job.
203
210
func (c * Client ) CommentOnIssue (ctx context.Context , r report.Report , issue * github.Issue ) * github.IssueComment {
204
- body := os .Expand (issueCommentTemplate , templateHelper (c .envVariables , r ))
211
+ // Get commit message and extract PR number
212
+ commitMessage := c .getCommitMessage (ctx )
213
+ prNumber := c .extractPRNumberFromCommitMessage (commitMessage )
214
+
215
+ body := os .Expand (issueCommentTemplate , templateHelper (c .envVariables , r , prNumber ))
205
216
206
217
issueComment , response , err := c .client .Issues .CreateComment (
207
218
ctx ,
@@ -220,6 +231,13 @@ func (c *Client) CommentOnIssue(ctx context.Context, r report.Report, issue *git
220
231
c .handleBadResponses (response )
221
232
}
222
233
234
+ // Also comment on the PR with a link to this comment
235
+ if prNumber > 0 && issueComment != nil && issueComment .HTMLURL != nil {
236
+ if prAuthor := c .GetPRAuthor (ctx , prNumber ); prAuthor != "" {
237
+ _ = c .CommentOnPR (ctx , prNumber , prAuthor , * issueComment .HTMLURL )
238
+ }
239
+ }
240
+
223
241
return issueComment
224
242
}
225
243
@@ -243,7 +261,7 @@ func getComponent(module string) string {
243
261
return module
244
262
}
245
263
246
- func templateHelper (env map [string ]string , r report.Report ) func (string ) string {
264
+ func templateHelper (env map [string ]string , r report.Report , prNumber int ) func (string ) string {
247
265
return func (param string ) string {
248
266
switch param {
249
267
case "jobName" :
@@ -257,6 +275,11 @@ func templateHelper(env map[string]string, r report.Report) func(string) string
257
275
return getComponent (trimmedModule )
258
276
case "commit" :
259
277
return shortSha (env [githubSHAKey ])
278
+ case "prNumber" :
279
+ if prNumber > 0 {
280
+ return fmt .Sprintf ("#%d" , prNumber )
281
+ }
282
+ return "N/A"
260
283
default :
261
284
return ""
262
285
}
@@ -271,11 +294,155 @@ func shortSha(sha string) string {
271
294
return sha
272
295
}
273
296
297
+ // getCommitMessage fetches the commit message
298
+ func (c * Client ) getCommitMessage (ctx context.Context ) string {
299
+ commit , response , err := c .client .Repositories .GetCommit (
300
+ ctx ,
301
+ c .envVariables [githubOwner ],
302
+ c .envVariables [githubRepository ],
303
+ c .envVariables [githubSHAKey ],
304
+ & github.ListOptions {},
305
+ )
306
+ if err != nil {
307
+ c .logger .Warn ("Failed to get commit message from GitHub API" ,
308
+ zap .String ("sha" , c .envVariables [githubSHAKey ]),
309
+ zap .Error (err ),
310
+ )
311
+ return ""
312
+ }
313
+
314
+ if response .StatusCode != http .StatusOK {
315
+ c .logger .Warn ("Unexpected response when fetching commit" ,
316
+ zap .Int ("status_code" , response .StatusCode ),
317
+ zap .String ("sha" , c .envVariables [githubSHAKey ]),
318
+ )
319
+ return ""
320
+ }
321
+
322
+ if commit .Commit != nil {
323
+ return * commit .Commit .Message
324
+ }
325
+
326
+ return ""
327
+ }
328
+
329
+ // GetPRAuthor fetches the author of a pull request
330
+ func (c * Client ) GetPRAuthor (ctx context.Context , prNumber int ) string {
331
+ if prNumber <= 0 {
332
+ return ""
333
+ }
334
+
335
+ pr , response , err := c .client .PullRequests .Get (
336
+ ctx ,
337
+ c .envVariables [githubOwner ],
338
+ c .envVariables [githubRepository ],
339
+ prNumber ,
340
+ )
341
+ if err != nil {
342
+ c .logger .Warn ("Failed to get PR details from GitHub API" ,
343
+ zap .Int ("pr_number" , prNumber ),
344
+ zap .Error (err ),
345
+ )
346
+ return ""
347
+ }
348
+
349
+ if response .StatusCode != http .StatusOK {
350
+ c .logger .Warn ("Unexpected response when fetching PR" ,
351
+ zap .Int ("status_code" , response .StatusCode ),
352
+ zap .Int ("pr_number" , prNumber ),
353
+ )
354
+ return ""
355
+ }
356
+
357
+ if pr .User != nil && pr .User .Login != nil {
358
+ return * pr .User .Login
359
+ }
360
+
361
+ return ""
362
+ }
363
+
364
+ // CommentOnPR adds a comment to a pull request to notify the author about failing tests
365
+ func (c * Client ) CommentOnPR (ctx context.Context , prNumber int , prAuthor string , issueURL string ) * github.IssueComment {
366
+ if prNumber <= 0 || prAuthor == "" {
367
+ c .logger .Warn ("Cannot comment on PR: missing PR number or author" ,
368
+ zap .Int ("pr_number" , prNumber ),
369
+ zap .String ("pr_author" , prAuthor ),
370
+ )
371
+ return nil
372
+ }
373
+
374
+ body := os .Expand (prCommentTemplate , func (param string ) string {
375
+ return prTemplateHelper (param , prAuthor , issueURL )
376
+ })
377
+
378
+ prComment , response , err := c .client .Issues .CreateComment (
379
+ ctx ,
380
+ c .envVariables [githubOwner ],
381
+ c .envVariables [githubRepository ],
382
+ prNumber ,
383
+ & github.IssueComment {
384
+ Body : & body ,
385
+ },
386
+ )
387
+ if err != nil {
388
+ c .logger .Warn ("Failed to comment on PR" ,
389
+ zap .Int ("pr_number" , prNumber ),
390
+ zap .Error (err ),
391
+ )
392
+ return nil
393
+ }
394
+
395
+ if response .StatusCode != http .StatusCreated {
396
+ c .logger .Warn ("Unexpected response when commenting on PR" ,
397
+ zap .Int ("status_code" , response .StatusCode ),
398
+ zap .Int ("pr_number" , prNumber ),
399
+ )
400
+ return nil
401
+ }
402
+
403
+ return prComment
404
+ }
405
+
406
+ func (c * Client ) extractPRNumberFromCommitMessage (commitMsg string ) int {
407
+ // Only consider the first line of the commit message.
408
+ firstLine := strings .SplitN (commitMsg , "\n " , 2 )[0 ]
409
+
410
+ // cases matched :
411
+ // - (#123)
412
+ // - Merge pull request #123
413
+ // - (#123): some description
414
+ // - pull request #123
415
+ prRegex := regexp .MustCompile (`(?i)(?:merge pull request #|pull request #|\(#)(\d+)\)?` )
416
+ matches := prRegex .FindStringSubmatch (firstLine )
417
+
418
+ if len (matches ) >= 2 {
419
+ prNumber , err := strconv .Atoi (matches [1 ])
420
+ if err != nil {
421
+ c .logger .Warn ("Failed to convert PR number to integer" ,
422
+ zap .String ("pr_string" , matches [1 ]),
423
+ zap .Error (err ),
424
+ )
425
+ return 0
426
+ }
427
+ return prNumber
428
+ }
429
+
430
+ c .logger .Warn ("No PR number found in commit message" ,
431
+ zap .String ("first_line" , firstLine ),
432
+ )
433
+ return 0
434
+ }
435
+
274
436
// CreateIssue creates a new GitHub Issue corresponding to a build failure.
275
437
func (c * Client ) CreateIssue (ctx context.Context , r report.Report ) * github.Issue {
276
438
trimmedModule := trimModule (c .envVariables [githubOwner ], c .envVariables [githubRepository ], r .Module )
277
439
title := strings .Replace (issueTitleTemplate , "${module}" , trimmedModule , 1 )
278
- body := os .Expand (issueBodyTemplate , templateHelper (c .envVariables , r ))
440
+
441
+ // Get commit message and extract PR number
442
+ commitMessage := c .getCommitMessage (ctx )
443
+ prNumber := c .extractPRNumberFromCommitMessage (commitMessage )
444
+
445
+ body := os .Expand (issueBodyTemplate , templateHelper (c .envVariables , r , prNumber ))
279
446
componentName := getComponent (trimmedModule )
280
447
281
448
issueLabels := c .cfg .labelsCopy ()
@@ -298,9 +465,27 @@ func (c *Client) CreateIssue(ctx context.Context, r report.Report) *github.Issue
298
465
c .handleBadResponses (response )
299
466
}
300
467
468
+ // After creating the issue, also comment on the PR with a link to the created issue
469
+ if prNumber > 0 && issue != nil && issue .HTMLURL != nil {
470
+ if prAuthor := c .GetPRAuthor (ctx , prNumber ); prAuthor != "" {
471
+ _ = c .CommentOnPR (ctx , prNumber , prAuthor , * issue .HTMLURL )
472
+ }
473
+ }
474
+
301
475
return issue
302
476
}
303
477
478
+ func prTemplateHelper (param string , prAuthor string , issueURL string ) string {
479
+ switch param {
480
+ case "prAuthor" :
481
+ return prAuthor
482
+ case "issueLink" :
483
+ return issueURL
484
+ default :
485
+ return ""
486
+ }
487
+ }
488
+
304
489
func (c * Client ) handleBadResponses (response * github.Response ) {
305
490
body , _ := io .ReadAll (response .Body )
306
491
c .logger .Fatal (
0 commit comments