From 659825e3e1135d1fb3a5a59e846b6c300317c19d Mon Sep 17 00:00:00 2001 From: Gnani Rahul Date: Thu, 16 Apr 2026 01:17:48 -0500 Subject: [PATCH 1/2] Fix Gitea pull request label support Signed-off-by: Gnani Rahul --- pkg/gitprovider/gitea/gitea.go | 85 ++++++++++++++++- pkg/gitprovider/gitea/gitea_test.go | 143 ++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 5 deletions(-) diff --git a/pkg/gitprovider/gitea/gitea.go b/pkg/gitprovider/gitea/gitea.go index 35aecffe7c..c74fcf8b00 100644 --- a/pkg/gitprovider/gitea/gitea.go +++ b/pkg/gitprovider/gitea/gitea.go @@ -62,12 +62,25 @@ type giteaClient interface { opts gitea.ListPullRequestsOptions, ) ([]*gitea.PullRequest, *gitea.Response, error) + ListRepoLabels( + owner string, + repo string, + opts gitea.ListLabelsOptions, + ) ([]*gitea.Label, *gitea.Response, error) + GetPullRequest( owner string, repo string, number int64, ) (*gitea.PullRequest, *gitea.Response, error) + AddIssueLabels( + owner string, + repo string, + number int64, + opts gitea.IssueLabelsOption, + ) ([]*gitea.Label, *gitea.Response, error) + MergePullRequest( owner string, repo string, @@ -133,6 +146,10 @@ func (p *provider) CreatePullRequest( if opts == nil { opts = &gitprovider.CreatePullRequestOpts{} } + labelIDs, err := p.resolveLabelIDs(opts.Labels) + if err != nil { + return nil, err + } giteaPR, _, err := p.client.CreatePullRequest( p.owner, p.repo, @@ -149,15 +166,73 @@ func (p *provider) CreatePullRequest( if giteaPR == nil { return nil, fmt.Errorf("unexpected nil pull request") } - // TODO(krancour): Add label support. The Gitea SDK's AddIssueLabels expects - // label IDs ([]int64), but Kargo's CreatePullRequestOpts.Labels are names - // ([]string). A previous implementation attempted this but silently discarded - // the labels entirely. To fix properly: list repo labels, match by name to - // get IDs, then call AddIssueLabels. + if len(labelIDs) > 0 { + if _, _, err := p.client.AddIssueLabels( + p.owner, + p.repo, + giteaPR.Index, + gitea.IssueLabelsOption{Labels: labelIDs}, + ); err != nil { + return nil, fmt.Errorf( + "error adding labels to pull request %d: %w", + giteaPR.Index, + err, + ) + } + } pr := convertGiteaPR(*giteaPR) return &pr, nil } +func (p *provider) resolveLabelIDs(labelNames []string) ([]int64, error) { + if len(labelNames) == 0 { + return nil, nil + } + + repoLabels, _, err := p.client.ListRepoLabels( + p.owner, + p.repo, + gitea.ListLabelsOptions{}, + ) + if err != nil { + return nil, fmt.Errorf("error listing repository labels: %w", err) + } + + labelIDsByName := make(map[string]int64, len(repoLabels)) + for _, repoLabel := range repoLabels { + if repoLabel == nil { + continue + } + labelIDsByName[repoLabel.Name] = repoLabel.ID + } + + labelIDs := make([]int64, 0, len(labelNames)) + seen := make(map[int64]struct{}, len(labelNames)) + missing := make([]string, 0) + for _, labelName := range labelNames { + labelID, ok := labelIDsByName[labelName] + if !ok { + missing = append(missing, labelName) + continue + } + if _, ok := seen[labelID]; ok { + continue + } + seen[labelID] = struct{}{} + labelIDs = append(labelIDs, labelID) + } + if len(missing) > 0 { + return nil, fmt.Errorf( + "labels not found in repository %s/%s: %s", + p.owner, + p.repo, + strings.Join(missing, ", "), + ) + } + + return labelIDs, nil +} + // GetPullRequest implements gitprovider.Interface. func (p *provider) GetPullRequest( _ context.Context, diff --git a/pkg/gitprovider/gitea/gitea_test.go b/pkg/gitprovider/gitea/gitea_test.go index 0c5795c946..29319d5de0 100644 --- a/pkg/gitprovider/gitea/gitea_test.go +++ b/pkg/gitprovider/gitea/gitea_test.go @@ -85,6 +85,7 @@ type mockGiteaClient struct { owner string repo string issueLabelsOptions gitea.IssueLabelsOption + repoLabelsOpts gitea.ListLabelsOptions listOpts gitea.ListPullRequestsOptions } @@ -127,6 +128,26 @@ func (m *mockGiteaClient) GetPullRequest( return pr, resp, args.Error(2) } +func (m *mockGiteaClient) ListRepoLabels( + owner string, + repo string, + opts gitea.ListLabelsOptions, +) ([]*gitea.Label, *gitea.Response, error) { + args := m.Called(owner, repo, opts) + m.owner = owner + m.repo = repo + m.repoLabelsOpts = opts + labels, ok := args.Get(0).([]*gitea.Label) + if !ok { + return nil, nil, args.Error(2) + } + resp, ok := args.Get(1).(*gitea.Response) + if !ok { + return labels, nil, args.Error(2) + } + return labels, resp, args.Error(2) +} + func (m *mockGiteaClient) AddIssueLabels( owner string, repo string, @@ -252,6 +273,128 @@ func TestCreatePullRequest(t *testing.T) { require.True(t, pr.Open) } +func TestCreatePullRequestWithLabels(t *testing.T) { + opts := gitprovider.CreatePullRequestOpts{ + Head: "feature-branch", + Base: "main", + Title: "title", + Description: "desc", + Labels: []string{"label1", "label2"}, + } + + mockClient := &mockGiteaClient{ + pr: &gitea.PullRequest{ + Index: int64(42), + State: gitea.StateOpen, + Head: &gitea.PRBranchInfo{ + Sha: "HeadSha", + }, + Base: &gitea.PRBranchInfo{ + Sha: "BaseSha", + }, + URL: "http://localhost:8080", + MergedCommitID: ptr.To("2994fd93"), + HasMerged: false, + }, + } + mockClient. + On("ListRepoLabels", testRepoOwner, testRepoName, gitea.ListLabelsOptions{}). + Return( + []*gitea.Label{ + {ID: 101, Name: "label1"}, + {ID: 202, Name: "label2"}, + }, + &gitea.Response{}, + nil, + ) + mockClient. + On("CreatePullRequest", testRepoOwner, testRepoName, mock.Anything). + Return( + &gitea.PullRequest{ + Index: int64(42), + State: gitea.StateOpen, + Head: &gitea.PRBranchInfo{ + Sha: "HeadSha", + }, + Base: &gitea.PRBranchInfo{ + Sha: "BaseSha", + }, + URL: "http://localhost:8080", + MergedCommitID: ptr.To("BaseSha"), + HasMerged: false, + Created: &time.Time{}, + }, + &gitea.Response{}, + nil, + ) + mockClient. + On( + "AddIssueLabels", + testRepoOwner, + testRepoName, + int64(42), + gitea.IssueLabelsOption{Labels: []int64{101, 202}}, + ). + Return([]*gitea.Label{}, &gitea.Response{}, nil) + + g := provider{ + owner: testRepoOwner, + repo: testRepoName, + client: mockClient, + } + pr, err := g.CreatePullRequest(t.Context(), &opts) + + mockClient.AssertExpectations(t) + + require.NoError(t, err) + require.Equal(t, testRepoOwner, mockClient.owner) + require.Equal(t, testRepoName, mockClient.repo) + require.Equal(t, opts.Head, mockClient.newPr.Head) + require.Equal(t, opts.Base, mockClient.newPr.Base) + require.Equal(t, opts.Title, mockClient.newPr.Title) + require.Equal(t, opts.Description, mockClient.newPr.Body) + require.Equal(t, []int64{101, 202}, mockClient.issueLabelsOptions.Labels) + require.Equal(t, mockClient.pr.Index, pr.Number) + require.Equal(t, mockClient.pr.Base.Sha, pr.MergeCommitSHA) + require.Equal(t, mockClient.pr.URL, pr.URL) + require.True(t, pr.Open) +} + +func TestCreatePullRequestWithMissingLabels(t *testing.T) { + opts := gitprovider.CreatePullRequestOpts{ + Head: "feature-branch", + Base: "main", + Title: "title", + Description: "desc", + Labels: []string{"label1", "missing"}, + } + + mockClient := &mockGiteaClient{} + mockClient. + On("ListRepoLabels", testRepoOwner, testRepoName, gitea.ListLabelsOptions{}). + Return( + []*gitea.Label{ + {ID: 101, Name: "label1"}, + }, + &gitea.Response{}, + nil, + ) + + g := provider{ + owner: testRepoOwner, + repo: testRepoName, + client: mockClient, + } + pr, err := g.CreatePullRequest(t.Context(), &opts) + + mockClient.AssertExpectations(t) + + require.Nil(t, pr) + require.ErrorContains(t, err, "labels not found") + mockClient.AssertNotCalled(t, "CreatePullRequest", mock.Anything, mock.Anything, mock.Anything) + mockClient.AssertNotCalled(t, "AddIssueLabels", mock.Anything, mock.Anything, mock.Anything, mock.Anything) +} + func TestGetPullRequest(t *testing.T) { // set up mock mockClient := &mockGiteaClient{ From fa8ec918cfd89be99c3e0e4e2b81733501b191b1 Mon Sep 17 00:00:00 2001 From: Gnani Rahul Date: Fri, 17 Apr 2026 15:04:39 -0500 Subject: [PATCH 2/2] fix: paginate Gitea pull request labels Signed-off-by: Gnani Rahul --- pkg/gitprovider/gitea/gitea.go | 54 +++++++---------- pkg/gitprovider/gitea/gitea_test.go | 89 ++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 45 deletions(-) diff --git a/pkg/gitprovider/gitea/gitea.go b/pkg/gitprovider/gitea/gitea.go index c74fcf8b00..9d12c64cf5 100644 --- a/pkg/gitprovider/gitea/gitea.go +++ b/pkg/gitprovider/gitea/gitea.go @@ -74,13 +74,6 @@ type giteaClient interface { number int64, ) (*gitea.PullRequest, *gitea.Response, error) - AddIssueLabels( - owner string, - repo string, - number int64, - opts gitea.IssueLabelsOption, - ) ([]*gitea.Label, *gitea.Response, error) - MergePullRequest( owner string, repo string, @@ -154,10 +147,11 @@ func (p *provider) CreatePullRequest( p.owner, p.repo, gitea.CreatePullRequestOption{ - Title: opts.Title, - Head: opts.Head, - Base: opts.Base, - Body: opts.Description, + Title: opts.Title, + Head: opts.Head, + Base: opts.Base, + Body: opts.Description, + Labels: labelIDs, }, ) if err != nil { @@ -166,20 +160,6 @@ func (p *provider) CreatePullRequest( if giteaPR == nil { return nil, fmt.Errorf("unexpected nil pull request") } - if len(labelIDs) > 0 { - if _, _, err := p.client.AddIssueLabels( - p.owner, - p.repo, - giteaPR.Index, - gitea.IssueLabelsOption{Labels: labelIDs}, - ); err != nil { - return nil, fmt.Errorf( - "error adding labels to pull request %d: %w", - giteaPR.Index, - err, - ) - } - } pr := convertGiteaPR(*giteaPR) return &pr, nil } @@ -189,13 +169,23 @@ func (p *provider) resolveLabelIDs(labelNames []string) ([]int64, error) { return nil, nil } - repoLabels, _, err := p.client.ListRepoLabels( - p.owner, - p.repo, - gitea.ListLabelsOptions{}, - ) - if err != nil { - return nil, fmt.Errorf("error listing repository labels: %w", err) + repoLabels := make([]*gitea.Label, 0, len(labelNames)) + for page := 1; ; { + pageLabels, resp, err := p.client.ListRepoLabels( + p.owner, + p.repo, + gitea.ListLabelsOptions{ + ListOptions: gitea.ListOptions{Page: page}, + }, + ) + if err != nil { + return nil, fmt.Errorf("error listing repository labels: %w", err) + } + repoLabels = append(repoLabels, pageLabels...) + if resp == nil || resp.NextPage == 0 { + break + } + page = resp.NextPage } labelIDsByName := make(map[string]int64, len(repoLabels)) diff --git a/pkg/gitprovider/gitea/gitea_test.go b/pkg/gitprovider/gitea/gitea_test.go index 29319d5de0..33119de37e 100644 --- a/pkg/gitprovider/gitea/gitea_test.go +++ b/pkg/gitprovider/gitea/gitea_test.go @@ -298,7 +298,14 @@ func TestCreatePullRequestWithLabels(t *testing.T) { }, } mockClient. - On("ListRepoLabels", testRepoOwner, testRepoName, gitea.ListLabelsOptions{}). + On( + "ListRepoLabels", + testRepoOwner, + testRepoName, + gitea.ListLabelsOptions{ + ListOptions: gitea.ListOptions{Page: 1}, + }, + ). Return( []*gitea.Label{ {ID: 101, Name: "label1"}, @@ -327,16 +334,6 @@ func TestCreatePullRequestWithLabels(t *testing.T) { &gitea.Response{}, nil, ) - mockClient. - On( - "AddIssueLabels", - testRepoOwner, - testRepoName, - int64(42), - gitea.IssueLabelsOption{Labels: []int64{101, 202}}, - ). - Return([]*gitea.Label{}, &gitea.Response{}, nil) - g := provider{ owner: testRepoOwner, repo: testRepoName, @@ -353,11 +350,19 @@ func TestCreatePullRequestWithLabels(t *testing.T) { require.Equal(t, opts.Base, mockClient.newPr.Base) require.Equal(t, opts.Title, mockClient.newPr.Title) require.Equal(t, opts.Description, mockClient.newPr.Body) - require.Equal(t, []int64{101, 202}, mockClient.issueLabelsOptions.Labels) + require.Equal(t, []int64{101, 202}, mockClient.newPr.Labels) require.Equal(t, mockClient.pr.Index, pr.Number) require.Equal(t, mockClient.pr.Base.Sha, pr.MergeCommitSHA) require.Equal(t, mockClient.pr.URL, pr.URL) require.True(t, pr.Open) + mockClient.AssertNotCalled( + t, + "AddIssueLabels", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ) } func TestCreatePullRequestWithMissingLabels(t *testing.T) { @@ -371,7 +376,14 @@ func TestCreatePullRequestWithMissingLabels(t *testing.T) { mockClient := &mockGiteaClient{} mockClient. - On("ListRepoLabels", testRepoOwner, testRepoName, gitea.ListLabelsOptions{}). + On( + "ListRepoLabels", + testRepoOwner, + testRepoName, + gitea.ListLabelsOptions{ + ListOptions: gitea.ListOptions{Page: 1}, + }, + ). Return( []*gitea.Label{ {ID: 101, Name: "label1"}, @@ -395,6 +407,57 @@ func TestCreatePullRequestWithMissingLabels(t *testing.T) { mockClient.AssertNotCalled(t, "AddIssueLabels", mock.Anything, mock.Anything, mock.Anything, mock.Anything) } +func TestResolveLabelIDsPagesThroughAllRepositoryLabels(t *testing.T) { + mockClient := &mockGiteaClient{} + mockClient. + On( + "ListRepoLabels", + testRepoOwner, + testRepoName, + gitea.ListLabelsOptions{ + ListOptions: gitea.ListOptions{Page: 1}, + }, + ). + Return( + []*gitea.Label{ + {ID: 101, Name: "label1"}, + }, + &gitea.Response{NextPage: 2}, + nil, + ). + Once() + mockClient. + On( + "ListRepoLabels", + testRepoOwner, + testRepoName, + gitea.ListLabelsOptions{ + ListOptions: gitea.ListOptions{Page: 2}, + }, + ). + Return( + []*gitea.Label{ + {ID: 202, Name: "label2"}, + }, + &gitea.Response{}, + nil, + ). + Once() + + g := provider{ + owner: testRepoOwner, + repo: testRepoName, + client: mockClient, + } + + labelIDs, err := g.resolveLabelIDs([]string{"label1", "label2"}) + + mockClient.AssertExpectations(t) + + require.NoError(t, err) + require.Equal(t, []int64{101, 202}, labelIDs) +} + func TestGetPullRequest(t *testing.T) { // set up mock mockClient := &mockGiteaClient{