diff --git a/pkg/github/issues.go b/pkg/github/issues.go index c82a07c3a9..35c56a57ae 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -104,15 +104,15 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason { // IssueFragment represents a fragment of an issue node in the GraphQL API. type IssueFragment struct { - Number githubv4.Int - Title githubv4.String - Body githubv4.String - State githubv4.String - DatabaseID int64 + Number githubv4.Int + Title githubv4.String + Body githubv4.String + State githubv4.String + DatabaseID int64 + AuthorAssociation githubv4.String Author struct { - Login githubv4.String - Association githubv4.String + Login githubv4.String } CreatedAt githubv4.DateTime UpdatedAt githubv4.DateTime diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index d06721be72..003a511305 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -1130,14 +1130,15 @@ func Test_ListIssues(t *testing.T) { // Mock issues data mockIssuesAll := []map[string]any{ { - "number": 123, - "title": "First Issue", - "body": "This is the first test issue", - "state": "OPEN", - "databaseId": 1001, - "createdAt": "2023-01-01T00:00:00Z", - "updatedAt": "2023-01-01T00:00:00Z", - "author": map[string]any{"login": "user1"}, + "number": 123, + "title": "First Issue", + "body": "This is the first test issue", + "state": "OPEN", + "databaseId": 1001, + "authorAssociation": "MEMBER", + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2023-01-01T00:00:00Z", + "author": map[string]any{"login": "user1"}, "labels": map[string]any{ "nodes": []map[string]any{ {"name": "bug", "id": "label1", "description": "Bug label"}, @@ -1148,14 +1149,15 @@ func Test_ListIssues(t *testing.T) { }, }, { - "number": 456, - "title": "Second Issue", - "body": "This is the second test issue", - "state": "OPEN", - "databaseId": 1002, - "createdAt": "2023-02-01T00:00:00Z", - "updatedAt": "2023-02-01T00:00:00Z", - "author": map[string]any{"login": "user2"}, + "number": 456, + "title": "Second Issue", + "body": "This is the second test issue", + "state": "OPEN", + "databaseId": 1002, + "authorAssociation": "CONTRIBUTOR", + "createdAt": "2023-02-01T00:00:00Z", + "updatedAt": "2023-02-01T00:00:00Z", + "author": map[string]any{"login": "user2"}, "labels": map[string]any{ "nodes": []map[string]any{ {"name": "enhancement", "id": "label2", "description": "Enhancement label"}, @@ -1170,14 +1172,15 @@ func Test_ListIssues(t *testing.T) { mockIssuesOpen := []map[string]any{mockIssuesAll[0], mockIssuesAll[1]} mockIssuesClosed := []map[string]any{ { - "number": 789, - "title": "Closed Issue", - "body": "This is a closed issue", - "state": "CLOSED", - "databaseId": 1003, - "createdAt": "2023-03-01T00:00:00Z", - "updatedAt": "2023-03-01T00:00:00Z", - "author": map[string]any{"login": "user3"}, + "number": 789, + "title": "Closed Issue", + "body": "This is a closed issue", + "state": "CLOSED", + "databaseId": 1003, + "authorAssociation": "NONE", + "createdAt": "2023-03-01T00:00:00Z", + "updatedAt": "2023-03-01T00:00:00Z", + "author": map[string]any{"login": "user3"}, "labels": map[string]any{ "nodes": []map[string]any{}, }, @@ -1355,8 +1358,8 @@ func Test_ListIssues(t *testing.T) { } // Define the actual query strings that match the implementation - qBasicNoLabels := "query($after:String$direction:OrderDirection!$first:Int!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" - qWithLabels := "query($after:String$direction:OrderDirection!$first:Int!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qBasicNoLabels := "query($after:String$direction:OrderDirection!$first:Int!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}){nodes{number,title,body,state,databaseId,authorAssociation,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" + qWithLabels := "query($after:String$direction:OrderDirection!$first:Int!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}){nodes{number,title,body,state,databaseId,authorAssociation,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}" for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -1423,6 +1426,15 @@ func Test_ListIssues(t *testing.T) { assert.NotEmpty(t, issue.User.Login, "Issue user should have login") assert.Empty(t, issue.HTMLURL, "html_url should be empty (not populated by GraphQL fragment)") + switch issue.Number { + case 123: + assert.Equal(t, "MEMBER", issue.AuthorAssociation) + case 456: + assert.Equal(t, "CONTRIBUTOR", issue.AuthorAssociation) + case 789: + assert.Equal(t, "NONE", issue.AuthorAssociation) + } + // Labels should be flattened to name strings for _, label := range issue.Labels { assert.NotEmpty(t, label, "Label should be a non-empty string") diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index f22212935f..74f531a256 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -246,6 +246,7 @@ type MinimalPullRequest struct { MergeableState string `json:"mergeable_state,omitempty"` HTMLURL string `json:"html_url"` User *MinimalUser `json:"user,omitempty"` + AuthorAssociation string `json:"author_association,omitempty"` Labels []string `json:"labels,omitempty"` Assignees []string `json:"assignees,omitempty"` RequestedReviewers []string `json:"requested_reviewers,omitempty"` @@ -397,7 +398,7 @@ func fragmentToMinimalIssue(fragment IssueFragment) MinimalIssue { User: &MinimalUser{ Login: string(fragment.Author.Login), }, - AuthorAssociation: string(fragment.Author.Association), + AuthorAssociation: string(fragment.AuthorAssociation), } for _, label := range fragment.Labels.Nodes { @@ -503,9 +504,10 @@ func convertToMinimalPullRequest(pr *github.PullRequest) MinimalPullRequest { Draft: pr.GetDraft(), Merged: pr.GetMerged(), MergeableState: pr.GetMergeableState(), - HTMLURL: pr.GetHTMLURL(), - User: convertToMinimalUser(pr.GetUser()), - Additions: pr.GetAdditions(), + HTMLURL: pr.GetHTMLURL(), + User: convertToMinimalUser(pr.GetUser()), + AuthorAssociation: pr.GetAuthorAssociation(), + Additions: pr.GetAdditions(), Deletions: pr.GetDeletions(), ChangedFiles: pr.GetChangedFiles(), Commits: pr.GetCommits(), diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 801122dca8..c3d32dc653 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -50,6 +50,7 @@ func Test_GetPullRequest(t *testing.T) { User: &github.User{ Login: github.Ptr("testuser"), }, + AuthorAssociation: github.Ptr("MEMBER"), } tests := []struct { @@ -135,6 +136,7 @@ func Test_GetPullRequest(t *testing.T) { assert.Equal(t, tc.expectedPR.GetTitle(), returnedPR.Title) assert.Equal(t, tc.expectedPR.GetState(), returnedPR.State) assert.Equal(t, tc.expectedPR.GetHTMLURL(), returnedPR.HTMLURL) + assert.Equal(t, tc.expectedPR.GetAuthorAssociation(), returnedPR.AuthorAssociation) }) } } @@ -575,16 +577,18 @@ func Test_ListPullRequests(t *testing.T) { // Setup mock PRs for success case mockPRs := []*github.PullRequest{ { - Number: github.Ptr(42), - Title: github.Ptr("First PR"), - State: github.Ptr("open"), - HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), + Number: github.Ptr(42), + Title: github.Ptr("First PR"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), + AuthorAssociation: github.Ptr("OWNER"), }, { - Number: github.Ptr(43), - Title: github.Ptr("Second PR"), - State: github.Ptr("closed"), - HTMLURL: github.Ptr("https://github.com/owner/repo/pull/43"), + Number: github.Ptr(43), + Title: github.Ptr("Second PR"), + State: github.Ptr("closed"), + HTMLURL: github.Ptr("https://github.com/owner/repo/pull/43"), + AuthorAssociation: github.Ptr("CONTRIBUTOR"), }, } @@ -678,9 +682,11 @@ func Test_ListPullRequests(t *testing.T) { assert.Equal(t, *tc.expectedPRs[0].Number, returnedPRs[0].Number) assert.Equal(t, *tc.expectedPRs[0].Title, returnedPRs[0].Title) assert.Equal(t, *tc.expectedPRs[0].State, returnedPRs[0].State) + assert.Equal(t, tc.expectedPRs[0].GetAuthorAssociation(), returnedPRs[0].AuthorAssociation) assert.Equal(t, *tc.expectedPRs[1].Number, returnedPRs[1].Number) assert.Equal(t, *tc.expectedPRs[1].Title, returnedPRs[1].Title) assert.Equal(t, *tc.expectedPRs[1].State, returnedPRs[1].State) + assert.Equal(t, tc.expectedPRs[1].GetAuthorAssociation(), returnedPRs[1].AuthorAssociation) }) } }