Skip to content

Commit dccabd2

Browse files
jelly-afkatoulme
andauthored
[issuegenerator] Ping pr author of failing test (#1267)
* add pr section in issue body * ping author of a failing test's pr --------- Co-authored-by: Antoine Toulme <[email protected]>
1 parent 25ee987 commit dccabd2

File tree

4 files changed

+253
-4
lines changed

4 files changed

+253
-4
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
2+
change_type: enhancement
3+
4+
# The name of the component, or a single word describing the area of concern, (e.g. crosslink)
5+
component: issuegenerator
6+
7+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
8+
note: issuegenerator now pings the author of a failing test's PR
9+
10+
# One or more tracking issues related to the change
11+
issues: [1182]
12+
13+
# (Optional) One or more lines of additional information to render under the primary note.
14+
# These lines will be padded with 2 spaces and then inserted directly into the document.
15+
# Use pipe (|) for multiline entries.
16+
subtext:

issuegenerator/internal/github/client.go

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"io"
2424
"net/http"
2525
"os"
26+
"regexp"
27+
"strconv"
2628
"strings"
2729

2830
"github.com/google/go-github/v75/github"
@@ -52,6 +54,7 @@ Auto-generated report for ${jobName} job build.
5254
5355
Link to failed build: ${linkToBuild}
5456
Commit: ${commit}
57+
PR: ${prNumber}
5558
5659
### Component(s)
5760
${component}
@@ -66,6 +69,10 @@ Link to latest failed build: ${linkToBuild}
6669
Commit: ${commit}
6770
6871
${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!
6976
`
7077
)
7178

@@ -201,7 +208,11 @@ func (c *Client) GetExistingIssue(ctx context.Context, module string) *github.Is
201208
// information about the latest failure. This method is expected to be
202209
// called only if there's an existing open Issue for the current job.
203210
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))
205216

206217
issueComment, response, err := c.client.Issues.CreateComment(
207218
ctx,
@@ -220,6 +231,13 @@ func (c *Client) CommentOnIssue(ctx context.Context, r report.Report, issue *git
220231
c.handleBadResponses(response)
221232
}
222233

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+
223241
return issueComment
224242
}
225243

@@ -243,7 +261,7 @@ func getComponent(module string) string {
243261
return module
244262
}
245263

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 {
247265
return func(param string) string {
248266
switch param {
249267
case "jobName":
@@ -257,6 +275,11 @@ func templateHelper(env map[string]string, r report.Report) func(string) string
257275
return getComponent(trimmedModule)
258276
case "commit":
259277
return shortSha(env[githubSHAKey])
278+
case "prNumber":
279+
if prNumber > 0 {
280+
return fmt.Sprintf("#%d", prNumber)
281+
}
282+
return "N/A"
260283
default:
261284
return ""
262285
}
@@ -271,11 +294,155 @@ func shortSha(sha string) string {
271294
return sha
272295
}
273296

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+
274436
// CreateIssue creates a new GitHub Issue corresponding to a build failure.
275437
func (c *Client) CreateIssue(ctx context.Context, r report.Report) *github.Issue {
276438
trimmedModule := trimModule(c.envVariables[githubOwner], c.envVariables[githubRepository], r.Module)
277439
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))
279446
componentName := getComponent(trimmedModule)
280447

281448
issueLabels := c.cfg.labelsCopy()
@@ -298,9 +465,27 @@ func (c *Client) CreateIssue(ctx context.Context, r report.Report) *github.Issue
298465
c.handleBadResponses(response)
299466
}
300467

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+
301475
return issue
302476
}
303477

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+
304489
func (c *Client) handleBadResponses(response *github.Response) {
305490
body, _ := io.ReadAll(response.Body)
306491
c.logger.Fatal(

issuegenerator/internal/github/client_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Auto-generated report for ` + "`test-ci`" + ` job build.
118118
119119
Link to failed build: https://github.com/test-org/test-repo/actions/runs/555555
120120
Commit: abcde12
121+
PR: N/A
121122
122123
### Component(s)
123124
` + "package1" + `
@@ -157,7 +158,7 @@ Commit: abcde12
157158
require.GreaterOrEqual(t, len(reports), len(tests))
158159
for i, tt := range tests {
159160
t.Run(tt.name, func(t *testing.T) {
160-
result := os.Expand(tt.template, templateHelper(envVariables, reports[i]))
161+
result := os.Expand(tt.template, templateHelper(envVariables, reports[i], 0))
161162
assert.Equal(t, tt.expected, result)
162163
})
163164
}
@@ -345,3 +346,48 @@ func TestNewClient(t *testing.T) {
345346
})
346347
}
347348
}
349+
350+
func TestExtractPRNumberFromMessage(t *testing.T) {
351+
type testCase struct {
352+
name string
353+
commitMsg string
354+
expectedPR int
355+
}
356+
tests := []testCase{
357+
{
358+
name: "Standard PR format (#123)",
359+
commitMsg: "Fix bug in receiver (#123)",
360+
expectedPR: 123,
361+
},
362+
{
363+
name: "Merge pull request #456",
364+
commitMsg: "Merge pull request #456 from branch/feature",
365+
expectedPR: 456,
366+
},
367+
{
368+
name: "pull request #321",
369+
commitMsg: "Some change pull request #321",
370+
expectedPR: 321,
371+
},
372+
{
373+
name: "No PR number",
374+
commitMsg: "Regular commit message",
375+
expectedPR: 0,
376+
},
377+
{
378+
name: "example otel commit message",
379+
commitMsg: "[chore] Skip test on Windows ARM (#42921)",
380+
expectedPR: 42921,
381+
},
382+
}
383+
384+
for _, tt := range tests {
385+
t.Run(tt.name, func(t *testing.T) {
386+
client := &Client{
387+
logger: zaptest.NewLogger(t),
388+
}
389+
prNum := client.extractPRNumberFromCommitMessage(tt.commitMsg)
390+
assert.Equal(t, tt.expectedPR, prNum)
391+
})
392+
}
393+
}

issuegenerator/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func main() {
6666
)
6767

6868
existingIssue := ghClient.GetExistingIssue(ctx, report.Module)
69+
// CreateIssue/CommentOnIssue will also comment on the related PR.
6970
if existingIssue == nil {
7071
// If none exists, create a new GitHub Issue for the failure.
7172
logger.Info("No existing Issues found, creating a new one.")
@@ -80,5 +81,6 @@ func main() {
8081
createdIssueComment := ghClient.CommentOnIssue(ctx, report, existingIssue)
8182
logger.Info("GitHub Issue updated", zap.String("html_url", *createdIssueComment.HTMLURL))
8283
}
84+
8385
}
8486
}

0 commit comments

Comments
 (0)