diff --git a/tools/pipeline/internal/pkg/github/add_assignees_test.go b/tools/pipeline/internal/pkg/github/add_assignees_test.go new file mode 100644 index 0000000000..7c40d95a10 --- /dev/null +++ b/tools/pipeline/internal/pkg/github/add_assignees_test.go @@ -0,0 +1,188 @@ +// Copyright IBM Corp. 2016, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package github + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + libgithub "github.com/google/go-github/v83/github" + "github.com/stretchr/testify/require" +) + +// Test_addAssignees tests the addAssignees helper function with various input scenarios +// including single/multiple assignees, empty lists, filtering of empty strings, and +// deduplication of logins. +func Test_addAssignees(t *testing.T) { + t.Parallel() + + for name, test := range map[string]struct { + logins []string + shouldCall bool + expectedError bool + }{ + "single assignee": { + logins: []string{"user1"}, + shouldCall: true, + }, + "multiple assignees": { + logins: []string{"user1", "user2", "user3"}, + shouldCall: true, + }, + "empty login list": { + logins: []string{}, + shouldCall: false, + }, + "nil login list": { + logins: nil, + shouldCall: false, + }, + "logins with empty strings": { + logins: []string{"user1", "", "user2", ""}, + shouldCall: true, // Empty strings should be filtered out + }, + "only empty strings": { + logins: []string{"", "", ""}, + shouldCall: false, // All empty, should skip + }, + "duplicate logins": { + logins: []string{"user1", "user1", "user2"}, + shouldCall: true, // Duplicates should be compacted + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + called := false + client, mux, teardown := setupTestClient(t) + defer teardown() + + if test.shouldCall { + mux.HandleFunc("/repos/test-owner/test-repo/issues/123/assignees", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + called = true + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"assignees": []}`)) + }) + } + + err := addAssignees( + context.Background(), + client, + "test-owner", + "test-repo", + 123, + test.logins, + ) + + if test.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, test.shouldCall, called, "API call expectation mismatch") + }) + } +} + +// Test_addReviewers tests the addReviewers helper function with various input scenarios +// including single/multiple reviewers, empty lists, filtering of empty strings, and +// deduplication of logins. +func Test_addReviewers(t *testing.T) { + t.Parallel() + + for name, test := range map[string]struct { + logins []string + shouldCall bool + expectedError bool + }{ + "single reviewer": { + logins: []string{"user1"}, + shouldCall: true, + }, + "multiple reviewers": { + logins: []string{"user1", "user2", "user3"}, + shouldCall: true, + }, + "empty login list": { + logins: []string{}, + shouldCall: false, + }, + "nil login list": { + logins: nil, + shouldCall: false, + }, + "logins with empty strings": { + logins: []string{"user1", "", "user2", ""}, + shouldCall: true, // Empty strings should be filtered out + }, + "only empty strings": { + logins: []string{"", "", ""}, + shouldCall: false, // All empty, should skip + }, + "duplicate logins": { + logins: []string{"user1", "user1", "user2"}, + shouldCall: true, // Duplicates should be compacted + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + called := false + client, mux, teardown := setupTestClient(t) + defer teardown() + + if test.shouldCall { + mux.HandleFunc("/repos/test-owner/test-repo/pulls/123/requested_reviewers", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + called = true + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"users": [], "teams": []}`)) + }) + } + + err := addReviewers( + context.Background(), + client, + "test-owner", + "test-repo", + 123, + test.logins, + ) + + if test.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + require.Equal(t, test.shouldCall, called, "API call expectation mismatch") + }) + } +} + +// setupTestClient creates a test GitHub client with a mock HTTP server for testing. +// It returns the client, the HTTP mux for registering handlers, and a teardown function +// that should be called to clean up the server when the test completes. +func setupTestClient(t *testing.T) (*libgithub.Client, *http.ServeMux, func()) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + client := libgithub.NewClient(nil) + serverURL, err := url.Parse(server.URL + "/") + require.NoError(t, err) + client.BaseURL = serverURL + + teardown := func() { + server.Close() + } + + return client, mux, teardown +} diff --git a/tools/pipeline/internal/pkg/github/add_reviewers.go b/tools/pipeline/internal/pkg/github/add_reviewers.go new file mode 100644 index 0000000000..e8e765f6b8 --- /dev/null +++ b/tools/pipeline/internal/pkg/github/add_reviewers.go @@ -0,0 +1,39 @@ +// Copyright IBM Corp. 2016, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package github + +import ( + "context" + "log/slog" + "slices" + + libgithub "github.com/google/go-github/v83/github" + slogctx "github.com/veqryn/slog-context" +) + +// addReviewers requests reviews from the given logins on the pull request +func addReviewers( + ctx context.Context, + github *libgithub.Client, + owner string, + repo string, + number int, + logins []string, +) error { + logins = slices.Compact(slices.DeleteFunc(logins, func(a string) bool { + return a == "" + })) + ctx = slogctx.Append(ctx, slog.Any("reviewer-logins", logins)) + + if len(logins) < 1 { + slog.Default().InfoContext(ctx, "skipping pull request review requests because no logins were provided") + return nil + } + + slog.Default().DebugContext(ctx, "requesting reviews on pull request") + _, _, err := github.PullRequests.RequestReviewers(ctx, owner, repo, number, libgithub.ReviewersRequest{ + Reviewers: logins, + }) + return err +} diff --git a/tools/pipeline/internal/pkg/github/create_backport.go b/tools/pipeline/internal/pkg/github/create_backport.go index 448a28d166..31441e5f46 100644 --- a/tools/pipeline/internal/pkg/github/create_backport.go +++ b/tools/pipeline/internal/pkg/github/create_backport.go @@ -759,6 +759,20 @@ func (r *CreateBackportReq) backportRef( return res } + // Request review from the PR author + err = addReviewers( + ctx, + github, + r.Owner, + r.Repo, + int(res.PullRequest.GetNumber()), + []string{pr.GetUser().GetLogin()}, + ) + if err != nil { + res.Error = fmt.Errorf("requesting review from PR author on backport pull request %w", err) + return res + } + // Copy non-backport labels from the original PR to the backport PR labelsToAdd := filterNonBackportLabels(pr.Labels, r.BackportLabelPrefix) err = addLabelsToIssue( diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 2d8033eb0d..6a05e1d402 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 3.0.0 '@hashicorp/vault-client-typescript': specifier: github:hashicorp/vault-client-typescript - version: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/0ba8e35e7fcf5b93110bb7fbcc2608184fd5ae07 + version: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/d0d038ea91aa92ecd0fd5d9e8f35b75a8c7e559c ember-auto-import: specifier: 2.10.0 version: 2.10.0(@glint/template@1.7.3)(webpack@5.105.4) @@ -1601,8 +1601,8 @@ packages: '@hashicorp/flight-icons@3.14.0': resolution: {integrity: sha512-nyLDApaZsAHpAf2sRNwYX1MnJQU9UI3euiwE6wHPl2l/+Yt8wba1oXkmWL/Ptc4QgJxxnRUUhf66jGcB/AIOyQ==} - '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/0ba8e35e7fcf5b93110bb7fbcc2608184fd5ae07': - resolution: {tarball: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/0ba8e35e7fcf5b93110bb7fbcc2608184fd5ae07} + '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/d0d038ea91aa92ecd0fd5d9e8f35b75a8c7e559c': + resolution: {tarball: https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/d0d038ea91aa92ecd0fd5d9e8f35b75a8c7e559c} version: 0.0.0 '@humanwhocodes/config-array@0.13.0': @@ -7201,8 +7201,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -11269,7 +11269,7 @@ snapshots: '@hashicorp/flight-icons@3.14.0': {} - '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/0ba8e35e7fcf5b93110bb7fbcc2608184fd5ae07': {} + '@hashicorp/vault-client-typescript@https://codeload.github.com/hashicorp/vault-client-typescript/tar.gz/d0d038ea91aa92ecd0fd5d9e8f35b75a8c7e559c': {} '@humanwhocodes/config-array@0.13.0': dependencies: @@ -18467,7 +18467,7 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: {} + semver@7.8.1: {} send@0.19.0: dependencies: @@ -19420,7 +19420,7 @@ snapshots: espree: 9.6.1 esquery: 1.7.0 lodash: 4.18.0 - semver: 7.8.0 + semver: 7.8.1 transitivePeerDependencies: - supports-color