diff --git a/apps/backend/internal/github/client_helpers.go b/apps/backend/internal/github/client_helpers.go index 7df629414a..2ac9b07938 100644 --- a/apps/backend/internal/github/client_helpers.go +++ b/apps/backend/internal/github/client_helpers.go @@ -399,11 +399,17 @@ func isNewerCheck(a, b CheckRun) bool { } // convertSearchItemToPR converts common search result fields into a PR struct. +// GitHub's /search/issues API returns merged PRs with state "closed" and a +// non-empty pull_request.merged_at; promote those to the "merged" state so the +// UI renders the purple merged icon instead of the red closed one. func convertSearchItemToPR( - number int, title, htmlURL, state, authorLogin, repositoryURL string, + number int, title, htmlURL, state, authorLogin, repositoryURL, mergedAt string, draft bool, createdAt, updatedAt time.Time, ) *PR { owner, repo := parseRepoURL(repositoryURL) + if mergedAt != "" { + state = prStateMerged + } return &PR{ Number: number, Title: title, @@ -415,6 +421,7 @@ func convertSearchItemToPR( RepoName: repo, CreatedAt: createdAt, UpdatedAt: updatedAt, + MergedAt: parseTimePtr(mergedAt), } } diff --git a/apps/backend/internal/github/client_helpers_test.go b/apps/backend/internal/github/client_helpers_test.go index 5a869c4d92..f95da72c2c 100644 --- a/apps/backend/internal/github/client_helpers_test.go +++ b/apps/backend/internal/github/client_helpers_test.go @@ -322,7 +322,7 @@ func TestConvertSearchItemToPR(t *testing.T) { now := time.Now() pr := convertSearchItemToPR( 42, "Fix bug", "https://github.com/owner/repo/pull/42", "open", - "alice", "https://api.github.com/repos/myorg/myrepo", false, + "alice", "https://api.github.com/repos/myorg/myrepo", "", false, now, now, ) @@ -338,6 +338,29 @@ func TestConvertSearchItemToPR(t *testing.T) { if pr.RepoName != "myrepo" { t.Errorf("expected repo myrepo, got %s", pr.RepoName) } + if pr.State != "open" { + t.Errorf("expected state open, got %s", pr.State) + } +} + +// GitHub's search API returns merged PRs with state "closed" plus a non-empty +// pull_request.merged_at. The converter must promote those to "merged" so the +// UI shows the purple merged icon instead of the red closed one. +func TestConvertSearchItemToPRMerged(t *testing.T) { + now := time.Now() + mergedAt := "2025-01-02T03:04:05Z" + pr := convertSearchItemToPR( + 7, "Land feature", "https://github.com/owner/repo/pull/7", "closed", + "bob", "https://api.github.com/repos/myorg/myrepo", mergedAt, false, + now, now, + ) + + if pr.State != prStateMerged { + t.Errorf("expected state %q, got %q", prStateMerged, pr.State) + } + if pr.MergedAt == nil { + t.Fatal("expected MergedAt to be set, got nil") + } } func TestLatestReviewByAuthor(t *testing.T) { diff --git a/apps/backend/internal/github/gh_client.go b/apps/backend/internal/github/gh_client.go index 14b9a52503..07983d35d6 100644 --- a/apps/backend/internal/github/gh_client.go +++ b/apps/backend/internal/github/gh_client.go @@ -1049,7 +1049,8 @@ type ghSearchItem struct { Login string `json:"login"` } `json:"user"` PullRequest struct { - URL string `json:"url"` + URL string `json:"url"` + MergedAt string `json:"merged_at"` } `json:"pull_request"` RepositoryURL string `json:"repository_url"` } @@ -1063,8 +1064,8 @@ func (c *GHClient) parseSearchResults(data string) ([]*PR, error) { for i, item := range items { prs[i] = convertSearchItemToPR( item.Number, item.Title, item.HTMLURL, item.State, - item.User.Login, item.RepositoryURL, item.Draft, - item.CreatedAt, item.UpdatedAt, + item.User.Login, item.RepositoryURL, item.PullRequest.MergedAt, + item.Draft, item.CreatedAt, item.UpdatedAt, ) } return prs, nil diff --git a/apps/backend/internal/github/pat_client.go b/apps/backend/internal/github/pat_client.go index a260dce0aa..5e370801e9 100644 --- a/apps/backend/internal/github/pat_client.go +++ b/apps/backend/internal/github/pat_client.go @@ -222,8 +222,8 @@ func (c *PATClient) ListReviewRequestedPRs(ctx context.Context, scope, filter, c for i, item := range result.Items { prs[i] = convertSearchItemToPR( item.Number, item.Title, item.HTMLURL, item.State, - item.User.Login, item.RepositoryURL, item.Draft, - item.CreatedAt, item.UpdatedAt, + item.User.Login, item.RepositoryURL, item.PullRequest.MergedAt, + item.Draft, item.CreatedAt, item.UpdatedAt, ) } return prs, nil @@ -253,8 +253,8 @@ func (c *PATClient) SearchPRsPaged(ctx context.Context, filter, customQuery stri for i, item := range result.Items { prs[i] = convertSearchItemToPR( item.Number, item.Title, item.HTMLURL, item.State, - item.User.Login, item.RepositoryURL, item.Draft, - item.CreatedAt, item.UpdatedAt, + item.User.Login, item.RepositoryURL, item.PullRequest.MergedAt, + item.Draft, item.CreatedAt, item.UpdatedAt, ) } return &PRSearchPage{PRs: prs, TotalCount: result.TotalCount, Page: page, PerPage: perPage}, nil @@ -946,6 +946,9 @@ type patSearchItem struct { User struct { Login string `json:"login"` } `json:"user"` + PullRequest struct { + MergedAt string `json:"merged_at"` + } `json:"pull_request"` } func convertPatPR(raw *patPR, owner, repo string) *PR {