From 350a1d642830ed79e7326c6130248457d716b66a Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 11 Sep 2025 01:28:55 +0900 Subject: [PATCH 001/126] build: customizable install `prefix` --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f823f6e93..5529113d4 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ endif ## Install/uninstall tasks are here for use on *nix platform. On Windows, there is no equivalent. DESTDIR := -prefix := /usr/local +prefix ?= /usr/local bindir := ${prefix}/bin datadir := ${prefix}/share mandir := ${datadir}/man From 3f0044fd94ace79714ee93df89b5efbd5b8bf242 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Fri, 26 Dec 2025 14:43:52 +0800 Subject: [PATCH 002/126] fix: error when --remote flag used with repo argument When a repository argument is provided to `gh repo fork`, the command operates independently of the current local repository. Using --remote in this context is incompatible because there's no local repository to add the remote to. This change returns an explicit error when these flags are combined, providing clear feedback instead of silently ignoring the --remote flag. Fixes #2722 Signed-off-by: majiayu000 <1835304752@qq.com> --- pkg/cmd/repo/fork/fork.go | 4 ++++ pkg/cmd/repo/fork/fork_test.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index b620291d6..3ebb02413 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -113,6 +113,10 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman opts.Rename = true // Any existing 'origin' will be renamed to upstream } + if opts.Repository != "" && cmd.Flags().Changed("remote") { + return cmdutil.FlagErrorf("the `--remote` flag is unsupported when a repository argument is provided") + } + if promptOk { // We can prompt for these if they were not specified. opts.PromptClone = !cmd.Flags().Changed("clone") diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 1f0b9cef1..edf5f2763 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -144,6 +144,12 @@ func TestNewCmdFork(t *testing.T) { Rename: false, }, }, + { + name: "remote with repo argument", + cli: "foo/bar --remote", + wantErr: true, + errMsg: "the `--remote` flag is unsupported when a repository argument is provided", + }, } for _, tt := range tests { From 6f739036b8aa23936a89a7e69a4fb2f7acf647af Mon Sep 17 00:00:00 2001 From: elijahthis Date: Sun, 1 Feb 2026 23:12:01 +0100 Subject: [PATCH 003/126] fix: clarify scope error while creating issues for projects --- api/client.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/api/client.go b/api/client.go index e6ff59c59..b83e9b707 100644 --- a/api/client.go +++ b/api/client.go @@ -10,6 +10,7 @@ import ( "regexp" "strings" + "github.com/cli/cli/v2/pkg/set" ghAPI "github.com/cli/go-gh/v2/pkg/api" ghauth "github.com/cli/go-gh/v2/pkg/auth" ) @@ -178,6 +179,10 @@ func handleResponse(err error) error { var gqlErr *ghAPI.GraphQLError if errors.As(err, &gqlErr) { + scopeErr := GenerateScopeErrorForGQL(gqlErr) + if scopeErr != nil { + return scopeErr + } return GraphQLError{ GraphQLError: gqlErr, } @@ -186,6 +191,40 @@ func handleResponse(err error) error { return err } +func GenerateScopeErrorForGQL(gqlErr *ghAPI.GraphQLError) error { + missing := set.NewStringSet() + for _, e := range gqlErr.Errors { + if e.Type != "INSUFFICIENT_SCOPES" { + continue + } + missing.AddValues(requiredScopesFromServerMessage(e.Message)) + } + if missing.Len() > 0 { + s := missing.ToSlice() + // TODO: this duplicates parts of generateScopesSuggestion + return fmt.Errorf( + "error: your authentication token is missing required scopes %v\n"+ + "To request it, run: gh auth refresh -s %s", + s, + strings.Join(s, ",")) + } + return nil +} + +var scopesRE = regexp.MustCompile(`one of the following scopes: \[(.+?)]`) + +func requiredScopesFromServerMessage(msg string) []string { + m := scopesRE.FindStringSubmatch(msg) + if m == nil { + return nil + } + var scopes []string + for _, mm := range strings.Split(m[1], ",") { + scopes = append(scopes, strings.Trim(mm, "' ")) + } + return scopes +} + // ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth // scopes in case a server response indicates that there are missing scopes. func ScopesSuggestion(resp *http.Response) string { From 71564fd4a15df4687e5d85962922067d69755b79 Mon Sep 17 00:00:00 2001 From: elijahthis Date: Sun, 1 Feb 2026 23:26:03 +0100 Subject: [PATCH 004/126] test: TestGenerateScopeErrorForGQL and TestRequiredScopesFromServerMessage --- api/client_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/api/client_test.go b/api/client_test.go index 1701a17a9..d961b05dc 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/go-gh/v2/pkg/api" "github.com/stretchr/testify/assert" ) @@ -255,3 +256,79 @@ func TestHTTPHeaders(t *testing.T) { } assert.Equal(t, "", stderr.String()) } + +func TestGenerateScopeErrorForGQL(t *testing.T) { + tests := []struct { + name string + gqlError *api.GraphQLError + wantErr bool + expected string + }{ + { + name: "missing scope", + gqlError: &api.GraphQLError{ + Errors: []api.GraphQLErrorItem{ + { + Type: "INSUFFICIENT_SCOPES", + Message: "The 'addProjectV2ItemById' field requires one of the following scopes: ['project']", + }, + }, + }, + wantErr: true, + expected: "error: your authentication token is missing required scopes [project]\n" + + "To request it, run: gh auth refresh -s project", + }, + + { + name: "ignore non-scope errors", + gqlError: &api.GraphQLError{ + Errors: []api.GraphQLErrorItem{ + { + Type: "NOT_FOUND", + Message: "Could not resolve to a Repository", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := GenerateScopeErrorForGQL(tt.gqlError) + if tt.wantErr { + assert.NotNil(t, err) + assert.Equal(t, tt.expected, err.Error()) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestRequiredScopesFromServerMessage(t *testing.T) { + tests := []struct { + msg string + expected []string + }{ + { + msg: "requires one of the following scopes: ['project']", + expected: []string{"project"}, + }, + { + msg: "requires one of the following scopes: ['repo', 'read:org']", + expected: []string{"repo", "read:org"}, + }, + { + msg: "no match here", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.msg, func(t *testing.T) { + output := requiredScopesFromServerMessage(tt.msg) + assert.Equal(t, tt.expected, output) + }) + } +} From fc8c0f1110171c3e4dffc77b3e8b7d4ed70785ba Mon Sep 17 00:00:00 2001 From: elijahthis Date: Tue, 3 Feb 2026 23:59:15 +0100 Subject: [PATCH 005/126] trigger rerun From 9904f7d1b9070bc22a1bccca0aa2c7ed150f2ec0 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:50:56 -0700 Subject: [PATCH 006/126] gh pr create: CCR and multiselectwithsearch --- api/queries_pr.go | 170 ++++++++-- api/queries_pr_test.go | 167 ++++++++++ pkg/cmd/issue/create/create.go | 2 +- pkg/cmd/pr/create/create.go | 28 +- pkg/cmd/pr/create/create_test.go | 548 +++++++++++++++++++------------ pkg/cmd/pr/shared/params.go | 42 ++- pkg/cmd/pr/shared/state.go | 1 + pkg/cmd/pr/shared/survey.go | 16 +- pkg/cmd/pr/shared/survey_test.go | 8 +- 9 files changed, 731 insertions(+), 251 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index bb5438fb3..632e7dd75 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -615,29 +615,41 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter } } - // reviewers are requested in yet another additional mutation - reviewParams := make(map[string]interface{}) - if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { - reviewParams["userIds"] = ids - } - if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { - reviewParams["teamIds"] = ids - } + // Request reviewers using either login-based (github.com) or ID-based (GHES) mutation + userLogins, hasUserLogins := params["userReviewerLogins"].([]string) + teamSlugs, hasTeamSlugs := params["teamReviewerSlugs"].([]string) - //TODO: How much work to extract this into own method and use for create and edit? - if len(reviewParams) > 0 { - reviewQuery := ` + if hasUserLogins || hasTeamSlugs { + // Use login-based mutation (RequestReviewsByLogin) for github.com + err := RequestReviewsByLogin(client, repo, pr.ID, userLogins, nil, teamSlugs, true) + if err != nil { + return pr, err + } + } else { + // Use ID-based mutation (requestReviews) for GHES compatibility + reviewParams := make(map[string]interface{}) + if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { + reviewParams["userIds"] = ids + } + if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { + reviewParams["teamIds"] = ids + } + + //TODO: How much work to extract this into own method and use for create and edit? + if len(reviewParams) > 0 { + reviewQuery := ` mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { requestReviews(input: $input) { clientMutationId } }` - reviewParams["pullRequestId"] = pr.ID - reviewParams["union"] = true - variables := map[string]interface{}{ - "input": reviewParams, - } - err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) - if err != nil { - return pr, err + reviewParams["pullRequestId"] = pr.ID + reviewParams["union"] = true + variables := map[string]interface{}{ + "input": reviewParams, + } + err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) + if err != nil { + return pr, err + } } } @@ -1109,6 +1121,126 @@ func SuggestedReviewerActors(client *Client, repo ghrepo.Interface, prID string, return candidates, moreResults, nil } +// SuggestedReviewerActorsForRepo fetches potential reviewers for a repository. +// Unlike SuggestedReviewerActors, this doesn't require an existing PR - used for gh pr create. +// It combines results from two sources using a cascading quota system: +// - repository collaborators (base quota: 5) +// - organization teams (base quota: 5 + unfilled from collaborators) +// +// This ensures we show up to 10 total candidates, with each source filling any +// unfilled quota from the previous source. Results are deduplicated. +// Returns the candidates, a MoreResults count, and an error. +func SuggestedReviewerActorsForRepo(client *Client, repo ghrepo.Interface, query string) ([]ReviewerCandidate, int, error) { + type responseData struct { + Repository struct { + // Check for Copilot availability by looking at any open PR's suggested reviewers + PullRequests struct { + Nodes []struct { + SuggestedActors struct { + Nodes []struct { + Reviewer struct { + TypeName string `graphql:"__typename"` + Bot struct { + Login string + } `graphql:"... on Bot"` + } + } + } `graphql:"suggestedReviewerActors(first: 10)"` + } + } `graphql:"pullRequests(first: 1, states: [OPEN])"` + Collaborators struct { + Nodes []struct { + Login string + Name string + } + } `graphql:"collaborators(first: 10, query: $query)"` + CollaboratorsTotalCount struct { + TotalCount int + } `graphql:"collaboratorsTotalCount: collaborators(first: 0)"` + } `graphql:"repository(owner: $owner, name: $name)"` + Organization struct { + Teams struct { + Nodes []struct { + Slug string + } + } `graphql:"teams(first: 10, query: $query)"` + TeamsTotalCount struct { + TotalCount int + } `graphql:"teamsTotalCount: teams(first: 0)"` + } `graphql:"organization(login: $owner)"` + } + + variables := map[string]interface{}{ + "query": githubv4.String(query), + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + var result responseData + err := client.Query(repo.RepoHost(), "SuggestedReviewerActorsForRepo", &result, variables) + // Handle the case where the owner is not an organization - the query still returns + // partial data (repository), so we can continue processing. + if err != nil && !strings.Contains(err.Error(), errorResolvingOrganization) { + return nil, 0, err + } + + // Build candidates using cascading quota logic + seen := make(map[string]bool) + var candidates []ReviewerCandidate + const baseQuota = 5 + + // Check for Copilot availability from open PR's suggested reviewers + for _, pr := range result.Repository.PullRequests.Nodes { + for _, actor := range pr.SuggestedActors.Nodes { + if actor.Reviewer.TypeName == "Bot" && actor.Reviewer.Bot.Login == CopilotReviewerLogin { + candidates = append(candidates, NewReviewerBot(CopilotReviewerLogin)) + seen[CopilotReviewerLogin] = true + break + } + } + } + + // Collaborators + collaboratorsAdded := 0 + for _, c := range result.Repository.Collaborators.Nodes { + if collaboratorsAdded >= baseQuota { + break + } + if c.Login == "" { + continue + } + if !seen[c.Login] { + seen[c.Login] = true + candidates = append(candidates, NewReviewerUser(c.Login, c.Name)) + collaboratorsAdded++ + } + } + + // Teams: quota = base + unfilled from collaborators + teamsQuota := baseQuota + (baseQuota - collaboratorsAdded) + teamsAdded := 0 + ownerName := repo.RepoOwner() + for _, t := range result.Organization.Teams.Nodes { + if teamsAdded >= teamsQuota { + break + } + if t.Slug == "" { + continue + } + teamLogin := fmt.Sprintf("%s/%s", ownerName, t.Slug) + if !seen[teamLogin] { + seen[teamLogin] = true + candidates = append(candidates, NewReviewerTeam(ownerName, t.Slug)) + teamsAdded++ + } + } + + // MoreResults uses unfiltered total counts (teams will be 0 for personal repos) + moreResults := result.Repository.CollaboratorsTotalCount.TotalCount + result.Organization.TeamsTotalCount.TotalCount + + return candidates, moreResults, nil +} + func UpdatePullRequestBranch(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestBranchInput) error { var mutation struct { UpdatePullRequestBranch struct { diff --git a/api/queries_pr_test.go b/api/queries_pr_test.go index 69dc505ca..4ee312b5d 100644 --- a/api/queries_pr_test.go +++ b/api/queries_pr_test.go @@ -362,3 +362,170 @@ func TestSuggestedReviewerActors(t *testing.T) { }) } } + +// mockReviewerResponseForRepo generates a GraphQL response for SuggestedReviewerActorsForRepo tests. +// It creates collaborators (c1, c2...) and teams (team1, team2...). +func mockReviewerResponseForRepo(collabs, teams, totalCollabs, totalTeams int) string { + return mockReviewerResponseForRepoWithCopilot(collabs, teams, totalCollabs, totalTeams, false) +} + +// mockReviewerResponseForRepoWithCopilot generates a GraphQL response for SuggestedReviewerActorsForRepo tests. +// If copilotAvailable is true, includes Copilot in the first open PR's suggested reviewers. +func mockReviewerResponseForRepoWithCopilot(collabs, teams, totalCollabs, totalTeams int, copilotAvailable bool) string { + var collabNodes, teamNodes []string + + for i := 1; i <= collabs; i++ { + collabNodes = append(collabNodes, + fmt.Sprintf(`{"login": "c%d", "name": "C%d"}`, i, i)) + } + for i := 1; i <= teams; i++ { + teamNodes = append(teamNodes, + fmt.Sprintf(`{"slug": "team%d"}`, i)) + } + + pullRequestsJSON := `"pullRequests": {"nodes": []}` + if copilotAvailable { + pullRequestsJSON = `"pullRequests": {"nodes": [{"suggestedReviewerActors": {"nodes": [{"reviewer": {"__typename": "Bot", "login": "copilot-pull-request-reviewer"}}]}}]}` + } + + return fmt.Sprintf(`{ + "data": { + "repository": { + %s, + "collaborators": {"nodes": [%s]}, + "collaboratorsTotalCount": {"totalCount": %d} + }, + "organization": { + "teams": {"nodes": [%s]}, + "teamsTotalCount": {"totalCount": %d} + } + } + }`, pullRequestsJSON, strings.Join(collabNodes, ","), totalCollabs, + strings.Join(teamNodes, ","), totalTeams) +} + +func TestSuggestedReviewerActorsForRepo(t *testing.T) { + tests := []struct { + name string + httpStubs func(*httpmock.Registry) + expectedCount int + expectedLogins []string + expectedMore int + expectError bool + }{ + { + name: "both sources plentiful - 5 each from cascading quota", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepo(6, 6, 20, 10))) + }, + expectedCount: 10, + expectedLogins: []string{"c1", "c2", "c3", "c4", "c5", "OWNER/team1", "OWNER/team2", "OWNER/team3", "OWNER/team4", "OWNER/team5"}, + expectedMore: 30, + }, + { + name: "few collaborators - teams fill gap", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepo(2, 10, 2, 15))) + }, + expectedCount: 10, + expectedLogins: []string{"c1", "c2", "OWNER/team1", "OWNER/team2", "OWNER/team3", "OWNER/team4", "OWNER/team5", "OWNER/team6", "OWNER/team7", "OWNER/team8"}, + expectedMore: 17, + }, + { + name: "no collaborators - teams only", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepo(0, 10, 0, 20))) + }, + expectedCount: 10, + expectedLogins: []string{"OWNER/team1", "OWNER/team2", "OWNER/team3", "OWNER/team4", "OWNER/team5", "OWNER/team6", "OWNER/team7", "OWNER/team8", "OWNER/team9", "OWNER/team10"}, + expectedMore: 20, + }, + { + name: "personal repo - no organization teams", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(`{ + "data": { + "repository": { + "pullRequests": {"nodes": []}, + "collaborators": {"nodes": [{"login": "c1", "name": "C1"}]}, + "collaboratorsTotalCount": {"totalCount": 3} + }, + "organization": null + }, + "errors": [{"message": "Could not resolve to an Organization with the login of 'OWNER'."}] + }`)) + }, + expectedCount: 1, + expectedLogins: []string{"c1"}, + expectedMore: 3, + }, + { + name: "empty repo", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepo(0, 0, 0, 0))) + }, + expectedCount: 0, + expectedLogins: []string{}, + expectedMore: 0, + }, + { + name: "copilot available - prepended to candidates", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepoWithCopilot(3, 2, 5, 5, true))) + }, + expectedCount: 6, + expectedLogins: []string{"copilot-pull-request-reviewer", "c1", "c2", "c3", "OWNER/team1", "OWNER/team2"}, + expectedMore: 10, + }, + { + name: "copilot not available - not included", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query SuggestedReviewerActorsForRepo\b`), + httpmock.StringResponse(mockReviewerResponseForRepoWithCopilot(3, 2, 5, 5, false))) + }, + expectedCount: 5, + expectedLogins: []string{"c1", "c2", "c3", "OWNER/team1", "OWNER/team2"}, + expectedMore: 10, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + + client := newTestClient(reg) + repo, _ := ghrepo.FromFullName("OWNER/REPO") + + candidates, moreResults, err := SuggestedReviewerActorsForRepo(client, repo, "") + if tt.expectError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expectedCount, len(candidates), "candidate count mismatch") + assert.Equal(t, tt.expectedMore, moreResults, "moreResults mismatch") + + logins := make([]string, len(candidates)) + for i, c := range candidates { + logins[i] = c.Login() + } + assert.Equal(t, tt.expectedLogins, logins) + }) + } +} diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index bc38c52b3..e3f2bd5e9 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -312,7 +312,7 @@ func createRun(opts *CreateOptions) (err error) { Repo: baseRepo, State: &tb, } - err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support) + err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support, nil) if err != nil { return } diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 5b5d45f9c..d29240fe5 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -21,6 +21,7 @@ import ( fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -397,11 +398,36 @@ func createRun(opts *CreateOptions) error { client := ctx.Client + // Detect ActorIsAssignable feature to determine if we can use search-based + // reviewer selection (github.com) or need to use traditional ID-based selection (GHES) + issueFeatures, _ := opts.Detector.IssueFeatures() + var reviewerSearchFunc func(string) prompter.MultiSelectSearchResult + if issueFeatures.ActorIsAssignable { + // Create search function for reviewer selection using login-based API + reviewerSearchFunc = func(query string) prompter.MultiSelectSearchResult { + candidates, moreResults, err := api.SuggestedReviewerActorsForRepo(client, ctx.PRRefs.BaseRepo(), query) + if err != nil { + return prompter.MultiSelectSearchResult{Err: err} + } + keys := make([]string, len(candidates)) + labels := make([]string, len(candidates)) + for i, c := range candidates { + keys[i] = c.Login() + labels[i] = c.DisplayName() + } + return prompter.MultiSelectSearchResult{Keys: keys, Labels: labels, MoreResults: moreResults} + } + } + state, err := NewIssueState(*ctx, *opts) if err != nil { return err } + if issueFeatures.ActorIsAssignable { + state.ActorReviewers = true + } + var openURL string if opts.WebMode { @@ -568,7 +594,7 @@ func createRun(opts *CreateOptions) error { Repo: ctx.PRRefs.BaseRepo(), State: state, } - err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state, projectsV1Support) + err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state, projectsV1Support, reviewerSearchFunc) if err != nil { return err } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 5ccffd7a8..254b54528 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -434,92 +434,6 @@ func Test_createRun(t *testing.T) { }, expectedErrOut: "", }, - { - name: "dry-run-nontty-with-all-opts", - tty: false, - setup: func(opts *CreateOptions, t *testing.T) func() { - opts.TitleProvided = true - opts.BodyProvided = true - opts.Title = "TITLE" - opts.Body = "BODY" - opts.BaseBranch = "trunk" - opts.HeadBranch = "feature" - opts.Assignees = []string{"monalisa"} - opts.Labels = []string{"bug", "todo"} - opts.Projects = []string{"roadmap"} - opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} - opts.Milestone = "big one.oh" - opts.DryRun = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) - reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), - httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { - "nodes": [ - { "login": "hubot", "id": "HUBOTID", "name": "" }, - { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query RepositoryLabelList\b`), - httpmock.StringResponse(` - { "data": { "repository": { "labels": { - "nodes": [ - { "name": "TODO", "id": "TODOID" }, - { "name": "bug", "id": "BUGID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query RepositoryMilestoneList\b`), - httpmock.StringResponse(` - { "data": { "repository": { "milestones": { - "nodes": [ - { "title": "GA", "id": "GAID" }, - { "title": "Big One.oh", "id": "BIGONEID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query OrganizationTeamList\b`), - httpmock.StringResponse(` - { "data": { "organization": { "teams": { - "nodes": [ - { "slug": "core", "id": "COREID" }, - { "slug": "robots", "id": "ROBOTID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - mockRetrieveProjects(t, reg) - }, - expectedOutputs: []string{ - "Would have created a Pull Request with:", - `title: TITLE`, - `draft: false`, - `base: trunk`, - `head: feature`, - `labels: bug, todo`, - `reviewers: hubot, monalisa, /core, /robots`, - `assignees: monalisa`, - `milestones: big one.oh`, - `projects: roadmap`, - `maintainerCanModify: false`, - `body:`, - `BODY`, - ``, - }, - expectedErrOut: "", - }, { name: "dry-run-tty-with-default-base", tty: true, @@ -549,98 +463,6 @@ func Test_createRun(t *testing.T) { Dry Running pull request for feature into master in OWNER/REPO - `), - }, - { - name: "dry-run-tty-with-all-opts", - tty: true, - setup: func(opts *CreateOptions, t *testing.T) func() { - opts.TitleProvided = true - opts.BodyProvided = true - opts.Title = "TITLE" - opts.Body = "BODY" - opts.BaseBranch = "trunk" - opts.HeadBranch = "feature" - opts.Assignees = []string{"monalisa"} - opts.Labels = []string{"bug", "todo"} - opts.Projects = []string{"roadmap"} - opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} - opts.Milestone = "big one.oh" - opts.DryRun = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) - reg.Register( - httpmock.GraphQL(`query RepositoryAssignableUsers\b`), - httpmock.StringResponse(` - { "data": { "repository": { "assignableUsers": { - "nodes": [ - { "login": "hubot", "id": "HUBOTID", "name": "" }, - { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query RepositoryLabelList\b`), - httpmock.StringResponse(` - { "data": { "repository": { "labels": { - "nodes": [ - { "name": "TODO", "id": "TODOID" }, - { "name": "bug", "id": "BUGID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query RepositoryMilestoneList\b`), - httpmock.StringResponse(` - { "data": { "repository": { "milestones": { - "nodes": [ - { "title": "GA", "id": "GAID" }, - { "title": "Big One.oh", "id": "BIGONEID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - reg.Register( - httpmock.GraphQL(`query OrganizationTeamList\b`), - httpmock.StringResponse(` - { "data": { "organization": { "teams": { - "nodes": [ - { "slug": "core", "id": "COREID" }, - { "slug": "robots", "id": "ROBOTID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - mockRetrieveProjects(t, reg) - }, - expectedOutputs: []string{ - `Would have created a Pull Request with:`, - `Title: TITLE`, - `Draft: false`, - `Base: trunk`, - `Head: feature`, - `Labels: bug, todo`, - `Reviewers: hubot, monalisa, /core, /robots`, - `Assignees: monalisa`, - `Milestones: big one.oh`, - `Projects: roadmap`, - `MaintainerCanModify: false`, - `Body:`, - ``, - ` BODY `, - ``, - ``, - }, - expectedErrOut: heredoc.Doc(` - - Dry Running pull request for feature into trunk in OWNER/REPO - `), }, { @@ -1092,9 +914,6 @@ func Test_createRun(t *testing.T) { return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) reg.Register( httpmock.GraphQL(`query RepositoryAssignableUsers\b`), httpmock.StringResponse(` @@ -1128,17 +947,6 @@ func Test_createRun(t *testing.T) { "pageInfo": { "hasNextPage": false } } } } } `)) - reg.Register( - httpmock.GraphQL(`query OrganizationTeamList\b`), - httpmock.StringResponse(` - { "data": { "organization": { "teams": { - "nodes": [ - { "slug": "core", "id": "COREID" }, - { "slug": "robots", "id": "ROBOTID" } - ], - "pageInfo": { "hasNextPage": false } - } } } } - `)) mockRetrieveProjects(t, reg) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), @@ -1171,15 +979,15 @@ func Test_createRun(t *testing.T) { assert.Equal(t, "BIGONEID", inputs["milestoneId"]) })) reg.Register( - httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), + httpmock.GraphQL(`mutation RequestReviewsByLogin\b`), httpmock.GraphQLMutation(` - { "data": { "requestReviews": { + { "data": { "requestReviewsByLogin": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) - assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"]) - assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"]) + assert.Equal(t, []interface{}{"hubot", "monalisa"}, inputs["userLogins"]) + assert.Equal(t, []interface{}{"core", "robots"}, inputs["teamSlugs"]) assert.Equal(t, true, inputs["union"]) })) }, @@ -1679,6 +1487,327 @@ func Test_createRun(t *testing.T) { }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", }, + { + name: "request reviewers by login", + setup: func(opts *CreateOptions, t *testing.T) func() { + opts.TitleProvided = true + opts.BodyProvided = true + opts.Title = "my title" + opts.Body = "my body" + opts.Reviewers = []string{"hubot", "monalisa", "org/core", "org/robots"} + opts.HeadBranch = "feature" + return func() {} + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`mutation PullRequestCreate\b`), + httpmock.GraphQLMutation(` + { "data": { "createPullRequest": { "pullRequest": { + "URL": "https://github.com/OWNER/REPO/pull/12", + "id": "NEWPULLID" + } } } }`, + func(input map[string]interface{}) {})) + reg.Register( + httpmock.GraphQL(`mutation RequestReviewsByLogin\b`), + httpmock.GraphQLMutation(` + { "data": { "requestReviewsByLogin": { + "clientMutationId": "" + } } } + `, func(inputs map[string]interface{}) { + assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) + assert.Equal(t, []interface{}{"hubot", "monalisa"}, inputs["userLogins"]) + assert.Equal(t, []interface{}{"core", "robots"}, inputs["teamSlugs"]) + assert.Equal(t, true, inputs["union"]) + })) + }, + expectedOut: "https://github.com/OWNER/REPO/pull/12\n", + expectedErrOut: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + branch := "feature" + reg := &httpmock.Registry{} + reg.StubRepoInfoResponse("OWNER", "REPO", "master") + defer reg.Verify(t) + if tt.httpStubs != nil { + tt.httpStubs(reg, t) + } + + pm := &prompter.PrompterMock{} + + if tt.promptStubs != nil { + tt.promptStubs(pm) + } + + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + if !tt.customBranchConfig { + cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "") + } + + if tt.cmdStubs != nil { + tt.cmdStubs(cs) + } + + opts := CreateOptions{} + opts.Prompter = pm + + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.tty) + ios.SetStdinTTY(tt.tty) + ios.SetStderrTTY(tt.tty) + + browser := &browser.Stub{} + opts.IO = ios + opts.Browser = browser + opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } + opts.Remotes = func() (context.Remotes, error) { + return context.Remotes{ + { + Remote: &git.Remote{ + Name: "origin", + Resolved: "base", + }, + Repo: ghrepo.New("OWNER", "REPO"), + }, + }, nil + } + opts.Branch = func() (string, error) { + return branch, nil + } + opts.Finder = shared.NewMockFinder(branch, nil, nil) + opts.GitClient = &git.Client{ + GhPath: "some/path/gh", + GitPath: "some/path/git", + } + cleanSetup := func() {} + if tt.setup != nil { + cleanSetup = tt.setup(&opts, t) + } + defer cleanSetup() + + // All tests in this function use github.com behavior + opts.Detector = &fd.EnabledDetectorMock{} + + if opts.HeadBranch == "" { + cs.Register(`git status --porcelain`, 0, "") + } + + err := createRun(&opts) + output := &test.CmdOut{ + OutBuf: stdout, + ErrBuf: stderr, + BrowsedURL: browser.BrowsedURL(), + } + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + if tt.expectedOut != "" { + assert.Equal(t, tt.expectedOut, output.String()) + } + if len(tt.expectedOutputs) > 0 { + assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n")) + } + assert.Equal(t, tt.expectedErrOut, output.Stderr()) + assert.Equal(t, tt.expectedBrowse, output.BrowsedURL) + } + }) + } +} + +func Test_createRun_GHES(t *testing.T) { + tests := []struct { + name string + setup func(*CreateOptions, *testing.T) func() + cmdStubs func(*run.CommandStubber) + promptStubs func(*prompter.PrompterMock) + httpStubs func(*httpmock.Registry, *testing.T) + expectedOutputs []string + expectedOut string + expectedErrOut string + tty bool + customBranchConfig bool + }{ + { + name: "dry-run-nontty-with-all-opts", + tty: false, + setup: func(opts *CreateOptions, t *testing.T) func() { + opts.TitleProvided = true + opts.BodyProvided = true + opts.Title = "TITLE" + opts.Body = "BODY" + opts.BaseBranch = "trunk" + opts.HeadBranch = "feature" + opts.Assignees = []string{"monalisa"} + opts.Labels = []string{"bug", "todo"} + opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} + opts.Milestone = "big one.oh" + opts.DryRun = true + return func() {} + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.StringResponse(` + { "data": { "repository": { "assignableUsers": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "name": "" }, + { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query RepositoryLabelList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "labels": { + "nodes": [ + { "name": "TODO", "id": "TODOID" }, + { "name": "bug", "id": "BUGID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestones": { + "nodes": [ + { "title": "GA", "id": "GAID" }, + { "title": "Big One.oh", "id": "BIGONEID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query OrganizationTeamList\b`), + httpmock.StringResponse(` + { "data": { "organization": { "teams": { + "nodes": [ + { "slug": "core", "id": "COREID" }, + { "slug": "robots", "id": "ROBOTID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + }, + expectedOutputs: []string{ + "Would have created a Pull Request with:", + `title: TITLE`, + `draft: false`, + `base: trunk`, + `head: feature`, + `labels: bug, todo`, + `reviewers: hubot, monalisa, /core, /robots`, + `assignees: monalisa`, + `milestones: big one.oh`, + `maintainerCanModify: false`, + `body:`, + `BODY`, + ``, + }, + expectedErrOut: "", + }, + { + name: "dry-run-tty-with-all-opts", + tty: true, + setup: func(opts *CreateOptions, t *testing.T) func() { + opts.TitleProvided = true + opts.BodyProvided = true + opts.Title = "TITLE" + opts.Body = "BODY" + opts.BaseBranch = "trunk" + opts.HeadBranch = "feature" + opts.Assignees = []string{"monalisa"} + opts.Labels = []string{"bug", "todo"} + opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} + opts.Milestone = "big one.oh" + opts.DryRun = true + return func() {} + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) + reg.Register( + httpmock.GraphQL(`query RepositoryAssignableUsers\b`), + httpmock.StringResponse(` + { "data": { "repository": { "assignableUsers": { + "nodes": [ + { "login": "hubot", "id": "HUBOTID", "name": "" }, + { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query RepositoryLabelList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "labels": { + "nodes": [ + { "name": "TODO", "id": "TODOID" }, + { "name": "bug", "id": "BUGID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestones": { + "nodes": [ + { "title": "GA", "id": "GAID" }, + { "title": "Big One.oh", "id": "BIGONEID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query OrganizationTeamList\b`), + httpmock.StringResponse(` + { "data": { "organization": { "teams": { + "nodes": [ + { "slug": "core", "id": "COREID" }, + { "slug": "robots", "id": "ROBOTID" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + }, + expectedOutputs: []string{ + `Would have created a Pull Request with:`, + `Title: TITLE`, + `Draft: false`, + `Base: trunk`, + `Head: feature`, + `Labels: bug, todo`, + `Reviewers: hubot, monalisa, /core, /robots`, + `Assignees: monalisa`, + `Milestones: big one.oh`, + `MaintainerCanModify: false`, + `Body:`, + ``, + ` BODY `, + ``, + ``, + }, + expectedErrOut: heredoc.Doc(` + + Dry Running pull request for feature into trunk in OWNER/REPO + + `), + }, { name: "fetch org teams non-interactively if reviewer contains any team", setup: func(opts *CreateOptions, t *testing.T) func() { @@ -1951,11 +2080,10 @@ func Test_createRun(t *testing.T) { } opts := CreateOptions{} - opts.Detector = &fd.EnabledDetectorMock{} + opts.Detector = &fd.DisabledDetectorMock{} opts.Prompter = pm ios, _, stdout, stderr := iostreams.Test() - // TODO do i need to bother with this ios.SetStdoutTTY(tt.tty) ios.SetStdinTTY(tt.tty) ios.SetStderrTTY(tt.tty) @@ -1999,23 +2127,17 @@ func Test_createRun(t *testing.T) { err := createRun(&opts) output := &test.CmdOut{ - OutBuf: stdout, - ErrBuf: stderr, - BrowsedURL: browser.BrowsedURL(), + OutBuf: stdout, + ErrBuf: stderr, } - if tt.wantErr != "" { - assert.EqualError(t, err, tt.wantErr) - } else { - assert.NoError(t, err) - if tt.expectedOut != "" { - assert.Equal(t, tt.expectedOut, output.String()) - } - if len(tt.expectedOutputs) > 0 { - assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n")) - } - assert.Equal(t, tt.expectedErrOut, output.Stderr()) - assert.Equal(t, tt.expectedBrowse, output.BrowsedURL) + assert.NoError(t, err) + if tt.expectedOut != "" { + assert.Equal(t, tt.expectedOut, output.String()) } + if len(tt.expectedOutputs) > 0 { + assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n")) + } + assert.Equal(t, tt.expectedErrOut, output.Stderr()) }) } } diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 784b68cf9..70990f2bf 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -61,11 +61,14 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par return nil } + // When ActorReviewers is true, we use login-based mutation and don't need to resolve reviewer IDs. + needReviewerIDs := len(tb.Reviewers) > 0 && !tb.ActorReviewers + // Retrieve minimal information needed to resolve metadata if this was not previously cached from additional metadata survey. if tb.MetadataResult == nil { input := api.RepoMetadataInput{ - Reviewers: len(tb.Reviewers) > 0, - TeamReviewers: len(tb.Reviewers) > 0 && slices.ContainsFunc(tb.Reviewers, func(r string) bool { + Reviewers: needReviewerIDs, + TeamReviewers: needReviewerIDs && slices.ContainsFunc(tb.Reviewers, func(r string) bool { return strings.ContainsRune(r, '/') }), Assignees: len(tb.Assignees) > 0, @@ -124,17 +127,34 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par } } - userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) - if err != nil { - return fmt.Errorf("could not request reviewer: %w", err) - } - params["userReviewerIds"] = userReviewerIDs + // When ActorReviewers is true (github.com), pass logins directly for use with + // RequestReviewsByLogin mutation. Otherwise, resolve to IDs for GHES compatibility. + if tb.ActorReviewers { + params["userReviewerLogins"] = userReviewers + // Extract team slugs from org/slug format + teamSlugs := make([]string, len(teamReviewers)) + for i, t := range teamReviewers { + parts := strings.SplitN(t, "/", 2) + if len(parts) == 2 { + teamSlugs[i] = parts[1] + } else { + teamSlugs[i] = t + } + } + params["teamReviewerSlugs"] = teamSlugs + } else { + userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) + if err != nil { + return fmt.Errorf("could not request reviewer: %w", err) + } + params["userReviewerIds"] = userReviewerIDs - teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers) - if err != nil { - return fmt.Errorf("could not request reviewer: %w", err) + teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers) + if err != nil { + return fmt.Errorf("could not request reviewer: %w", err) + } + params["teamReviewerIds"] = teamReviewerIDs } - params["teamReviewerIds"] = teamReviewerIDs return nil } diff --git a/pkg/cmd/pr/shared/state.go b/pkg/cmd/pr/shared/state.go index b9f2c0293..0e5c31cdd 100644 --- a/pkg/cmd/pr/shared/state.go +++ b/pkg/cmd/pr/shared/state.go @@ -20,6 +20,7 @@ type IssueMetadataState struct { Draft bool ActorAssignees bool + ActorReviewers bool Body string Title string diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index e350671b9..7bcf3a4a7 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -154,7 +154,7 @@ type RepoMetadataFetcher interface { RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error) } -func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState, projectsV1Support gh.ProjectsV1Support) error { +func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState, projectsV1Support gh.ProjectsV1Support, reviewerSearchFunc func(string) prompter.MultiSelectSearchResult) error { isChosen := func(m string) bool { for _, c := range state.Metadata { if m == c { @@ -254,7 +254,19 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface }{} if isChosen("Reviewers") { - if len(reviewers) > 0 { + if reviewerSearchFunc != nil { + // Use search-based selection (github.com with ActorIsAssignable) + selectedReviewers, err := p.MultiSelectWithSearch( + "Reviewers", + "Search reviewers", + state.Reviewers, + []string{}, + reviewerSearchFunc) + if err != nil { + return err + } + values.Reviewers = selectedReviewers + } else if len(reviewers) > 0 { selected, err := p.MultiSelect("Reviewers", state.Reviewers, reviewers) if err != nil { return err diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go index 7097d0761..23ba96cef 100644 --- a/pkg/cmd/pr/shared/survey_test.go +++ b/pkg/cmd/pr/shared/survey_test.go @@ -71,7 +71,7 @@ func TestMetadataSurvey_selectAll(t *testing.T) { Assignees: []string{"hubot"}, Type: PRMetadata, } - err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported) + err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported, nil) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) @@ -117,7 +117,7 @@ func TestMetadataSurvey_keepExisting(t *testing.T) { Assignees: []string{"hubot"}, } - err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported) + err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported, nil) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) @@ -146,7 +146,7 @@ func TestMetadataSurveyProjectV1Deprecation(t *testing.T) { return []int{0}, nil }) - err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Supported) + err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Supported, nil) require.ErrorContains(t, err, "expected test error") require.True(t, fetcher.projectsV1Requested, "expected projectsV1 to be requested") @@ -167,7 +167,7 @@ func TestMetadataSurveyProjectV1Deprecation(t *testing.T) { return []int{0}, nil }) - err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Unsupported) + err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Unsupported, nil) require.ErrorContains(t, err, "expected test error") require.False(t, fetcher.projectsV1Requested, "expected projectsV1 not to be requested") From 04bf86a72c568faf503cc2a88a453be3a921329c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:04:20 -0700 Subject: [PATCH 007/126] Address PR review comments Address PR review comments: code consistency and DRY improvements - Add botTypeName const for consistency with teamTypeName - Create extractTeamSlugs helper using strings.SplitN to simplify team slug extraction logic - Replace duplicate code in AddPullRequestReviews and RemovePullRequestReviews with extractTeamSlugs helper - Fix ClientMutationId naming with explicit graphql tag for consistency with other mutations in the codebase --- api/queries_pr.go | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 632e7dd75..6ee7f0c00 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -317,6 +317,9 @@ type RequestedReviewer struct { } `json:"organization"` } +const teamTypeName = "Team" +const botTypeName = "Bot" + func (r RequestedReviewer) LoginOrSlug() string { if r.TypeName == teamTypeName { return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) @@ -331,7 +334,7 @@ func (r RequestedReviewer) DisplayName() string { if r.TypeName == teamTypeName { return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) } - if r.TypeName == "Bot" && r.Login == CopilotReviewerLogin { + if r.TypeName == botTypeName && r.Login == CopilotReviewerLogin { return "Copilot (AI)" } if r.Name != "" { @@ -340,8 +343,6 @@ func (r RequestedReviewer) DisplayName() string { return r.Login } -const teamTypeName = "Team" - func (r ReviewRequests) Logins() []string { logins := make([]string, len(r.Nodes)) for i, r := range r.Nodes { @@ -669,6 +670,20 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter return pr, nil } +// extractTeamSlugs extracts just the slug portion from team identifiers. +// Team identifiers can be in "org/slug" format; this returns just the slug. +func extractTeamSlugs(teams []string) []string { + slugs := make([]string, 0, len(teams)) + for _, t := range teams { + if t == "" { + continue + } + s := strings.SplitN(t, "/", 2) + slugs = append(slugs, s[len(s)-1]) + } + return slugs +} + // AddPullRequestReviews adds the given user and team reviewers to a pull request using the REST API. // Team identifiers can be in "org/slug" format. func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error { @@ -681,16 +696,6 @@ func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users = []string{} } - // Extract just the slug from org/slug format - teamSlugs := make([]string, 0, len(teams)) - for _, t := range teams { - if idx := strings.Index(t, "/"); idx >= 0 { - teamSlugs = append(teamSlugs, t[idx+1:]) - } else if t != "" { - teamSlugs = append(teamSlugs, t) - } - } - path := fmt.Sprintf( "repos/%s/%s/pulls/%d/requested_reviewers", url.PathEscape(repo.RepoOwner()), @@ -702,7 +707,7 @@ func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, TeamReviewers []string `json:"team_reviewers"` }{ Reviewers: users, - TeamReviewers: teamSlugs, + TeamReviewers: extractTeamSlugs(teams), } buf := &bytes.Buffer{} if err := json.NewEncoder(buf).Encode(body); err != nil { @@ -724,16 +729,6 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in users = []string{} } - // Extract just the slug from org/slug format - teamSlugs := make([]string, 0, len(teams)) - for _, t := range teams { - if idx := strings.Index(t, "/"); idx >= 0 { - teamSlugs = append(teamSlugs, t[idx+1:]) - } else if t != "" { - teamSlugs = append(teamSlugs, t) - } - } - path := fmt.Sprintf( "repos/%s/%s/pulls/%d/requested_reviewers", url.PathEscape(repo.RepoOwner()), @@ -745,7 +740,7 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in TeamReviewers []string `json:"team_reviewers"` }{ Reviewers: users, - TeamReviewers: teamSlugs, + TeamReviewers: extractTeamSlugs(teams), } buf := &bytes.Buffer{} if err := json.NewEncoder(buf).Encode(body); err != nil { @@ -770,7 +765,7 @@ func RequestReviewsByLogin(client *Client, repo ghrepo.Interface, prID string, u var mutation struct { RequestReviewsByLogin struct { - ClientMutationID string + ClientMutationId string `graphql:"clientMutationId"` } `graphql:"requestReviewsByLogin(input: $input)"` } From 78aaa877183b82f5aec3a3a89878b6468330696a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:18:49 -0700 Subject: [PATCH 008/126] Add toGitHubV4Strings helper to reduce code duplication --- api/queries_pr.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 6ee7f0c00..ccde36935 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -684,6 +684,16 @@ func extractTeamSlugs(teams []string) []string { return slugs } +// toGitHubV4Strings converts a string slice to a githubv4.String slice, +// optionally appending a suffix to each element. +func toGitHubV4Strings(strs []string, suffix string) []githubv4.String { + result := make([]githubv4.String, len(strs)) + for i, s := range strs { + result[i] = githubv4.String(s + suffix) + } + return result +} + // AddPullRequestReviews adds the given user and team reviewers to a pull request using the REST API. // Team identifiers can be in "org/slug" format. func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error { @@ -782,23 +792,14 @@ func RequestReviewsByLogin(client *Client, repo ghrepo.Interface, prID string, u Union: githubv4.Boolean(union), } - userLoginValues := make([]githubv4.String, len(userLogins)) - for i, l := range userLogins { - userLoginValues[i] = githubv4.String(l) - } + userLoginValues := toGitHubV4Strings(userLogins, "") input.UserLogins = &userLoginValues - botLoginValues := make([]githubv4.String, len(botLogins)) - for i, l := range botLogins { - // Bot logins require the [bot] suffix for the mutation - botLoginValues[i] = githubv4.String(l + "[bot]") - } + // Bot logins require the [bot] suffix for the mutation + botLoginValues := toGitHubV4Strings(botLogins, "[bot]") input.BotLogins = &botLoginValues - teamSlugValues := make([]githubv4.String, len(teamSlugs)) - for i, s := range teamSlugs { - teamSlugValues[i] = githubv4.String(s) - } + teamSlugValues := toGitHubV4Strings(teamSlugs, "") input.TeamSlugs = &teamSlugValues variables := map[string]interface{}{ From 28ed4bdbf059e9471ae5d7682b753a58e87cce26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:17:42 +0000 Subject: [PATCH 009/126] chore(deps): bump golang.org/x/text from 0.32.0 to 0.33.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.32.0 to 0.33.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.32.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3cab99c16..f6e3a0446 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( golang.org/x/crypto v0.46.0 golang.org/x/sync v0.19.0 golang.org/x/term v0.38.0 - golang.org/x/text v0.32.0 + golang.org/x/text v0.33.0 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 gopkg.in/h2non/gock.v1 v1.1.2 @@ -177,10 +177,10 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect ) diff --git a/go.sum b/go.sum index e10e5a7d9..e7a288ce6 100644 --- a/go.sum +++ b/go.sum @@ -578,8 +578,8 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -617,16 +617,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= From ad305b721753c57fb6f80df7e93441ebc46983c3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:19:27 -0700 Subject: [PATCH 010/126] update third party licenses --- third-party-licenses.darwin.md | 4 ++-- third-party-licenses.linux.md | 4 ++-- third-party-licenses.windows.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 22041ae31..d52c13599 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -172,12 +172,12 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) -- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.30.0:LICENSE)) +- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) -- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE)) +- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) - [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.78.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 5f70fddab..a051627f8 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -172,12 +172,12 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) -- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.30.0:LICENSE)) +- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) -- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE)) +- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) - [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.78.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 0b53e20a5..ca38b8d84 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -175,12 +175,12 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) -- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.30.0:LICENSE)) +- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) -- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE)) +- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) - [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.78.0/LICENSE)) From 4de27314a0a7fe6795375a5e6e999d58f58c8241 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:28:20 +0000 Subject: [PATCH 011/126] chore(deps): bump golang.org/x/term from 0.38.0 to 0.39.0 Bumps [golang.org/x/term](https://github.com/golang/term) from 0.38.0 to 0.39.0. - [Commits](https://github.com/golang/term/compare/v0.38.0...v0.39.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f6e3a0446..0075b3a05 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/zalando/go-keyring v0.2.6 golang.org/x/crypto v0.46.0 golang.org/x/sync v0.19.0 - golang.org/x/term v0.38.0 + golang.org/x/term v0.39.0 golang.org/x/text v0.33.0 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 @@ -179,7 +179,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect diff --git a/go.sum b/go.sum index e7a288ce6..c31e042c4 100644 --- a/go.sum +++ b/go.sum @@ -604,13 +604,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From 0a2841fb9d007b186971bdb1f10c861c8b1f35e2 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:30:46 -0700 Subject: [PATCH 012/126] update third party licenses --- third-party-licenses.darwin.md | 4 ++-- third-party-licenses.linux.md | 4 ++-- third-party-licenses.windows.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index d52c13599..3f30e42f9 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -175,8 +175,8 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) -- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) -- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) +- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.40.0:LICENSE)) +- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.39.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index a051627f8..3531ff7fa 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -175,8 +175,8 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) -- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) -- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) +- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.40.0:LICENSE)) +- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.39.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index ca38b8d84..0862134a4 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -178,8 +178,8 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) -- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE)) -- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE)) +- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.40.0:LICENSE)) +- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.39.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE)) - [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/ff82c1b0f217/googleapis/api/LICENSE)) - [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/0a764e51fe1b/googleapis/rpc/LICENSE)) From 18c67c9ccc0f9cd4e2f109e2b36fda1fec5f71f3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:44:34 -0700 Subject: [PATCH 013/126] bump go to 1.25.7 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0075b3a05..63ea141f8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cli/cli/v2 -go 1.25.6 +go 1.25.7 require ( github.com/AlecAivazis/survey/v2 v2.3.7 From 35828f44cd3567affac2bb59fbcab1cada17223d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:52:24 -0700 Subject: [PATCH 014/126] Add manual dispatch to bump-go workflow Enable manual runs of the Bump Go workflow by adding the workflow_dispatch trigger alongside the existing scheduled cron. This allows maintainers to trigger the bump process on-demand while keeping the daily 3 AM UTC schedule intact. --- .github/workflows/bump-go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/bump-go.yml b/.github/workflows/bump-go.yml index 827bbc608..f9647b210 100644 --- a/.github/workflows/bump-go.yml +++ b/.github/workflows/bump-go.yml @@ -2,6 +2,7 @@ name: Bump Go on: schedule: - cron: "0 3 * * *" # 3 AM UTC + workflow_dispatch: permissions: contents: write pull-requests: write From 7fb3e1ad80f690d26e14b623a2a581e97085418a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:54:51 +0000 Subject: [PATCH 015/126] chore(deps): bump golang.org/x/crypto from 0.46.0 to 0.47.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.46.0 to 0.47.0. - [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 63ea141f8..e639e6033 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yuin/goldmark v1.7.16 github.com/zalando/go-keyring v0.2.6 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.47.0 golang.org/x/sync v0.19.0 golang.org/x/term v0.39.0 golang.org/x/text v0.33.0 diff --git a/go.sum b/go.sum index c31e042c4..1bf25eb4c 100644 --- a/go.sum +++ b/go.sum @@ -572,8 +572,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= From cde6edcba2bb705a5478847ea9bfb8ea128a5d95 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:23:06 -0700 Subject: [PATCH 016/126] update third party licenses --- third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 3f30e42f9..818c86cda 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -171,7 +171,7 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) -- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) +- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.47.0:LICENSE)) - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 3531ff7fa..fa8a3478e 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -171,7 +171,7 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) -- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) +- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.47.0:LICENSE)) - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 0862134a4..5eada97f1 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -174,7 +174,7 @@ Some packages may only be included on certain architectures or operating systems - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([BSD-3-Clause](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.38.0/trace/LICENSE)) - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE)) -- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE)) +- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.47.0:LICENSE)) - [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.31.0:LICENSE)) - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE)) - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.19.0:LICENSE)) From b32b7eab3911caf9ee4a90e71d89ce7842a7d37c Mon Sep 17 00:00:00 2001 From: vishnuvv27 Date: Mon, 9 Feb 2026 15:31:55 +0530 Subject: [PATCH 017/126] Fix redundant API call in gh issue view --comments (#12606) --- .../view/fixtures/issueView_previewSingleComment.json | 8 ++++++-- pkg/cmd/issue/view/http.go | 11 +++++------ pkg/cmd/issue/view/view.go | 2 -- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/issue/view/fixtures/issueView_previewSingleComment.json b/pkg/cmd/issue/view/fixtures/issueView_previewSingleComment.json index be099c14b..8959acec6 100644 --- a/pkg/cmd/issue/view/fixtures/issueView_previewSingleComment.json +++ b/pkg/cmd/issue/view/fixtures/issueView_previewSingleComment.json @@ -138,10 +138,14 @@ ] } ], - "totalCount": 6 + "totalCount": 6, + "pageInfo": { + "hasNextPage": true, + "endCursor": "Y3Vyc29yOnYyOjg5" + } }, "url": "https://github.com/OWNER/REPO/issues/123" } } } -} +} \ No newline at end of file diff --git a/pkg/cmd/issue/view/http.go b/pkg/cmd/issue/view/http.go index 4adc71802..2982fbbe3 100644 --- a/pkg/cmd/issue/view/http.go +++ b/pkg/cmd/issue/view/http.go @@ -20,14 +20,13 @@ func preloadIssueComments(client *http.Client, repo ghrepo.Interface, issue *api } `graphql:"node(id: $id)"` } + if !issue.Comments.PageInfo.HasNextPage { + return nil + } + variables := map[string]interface{}{ "id": githubv4.ID(issue.ID), - "endCursor": (*githubv4.String)(nil), - } - if issue.Comments.PageInfo.HasNextPage { - variables["endCursor"] = githubv4.String(issue.Comments.PageInfo.EndCursor) - } else { - issue.Comments.Nodes = issue.Comments.Nodes[0:0] + "endCursor": githubv4.String(issue.Comments.PageInfo.EndCursor), } gql := api.NewClientFromHTTP(client) diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index 41c01ef40..e41ad6acf 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -144,8 +144,6 @@ func viewRun(opts *ViewOptions) error { } if lookupFields.Contains("comments") { - // FIXME: this re-fetches the comments connection even though the initial set of 100 were - // fetched in the previous request. err := preloadIssueComments(httpClient, baseRepo, issue) if err != nil { return err From b38f6772e5741b754ff88e8dd464293eadd083c4 Mon Sep 17 00:00:00 2001 From: gunadhya <6939749+gunadhya@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:31:22 +0530 Subject: [PATCH 018/126] Fix issue develop repeated invocation with named branches --- pkg/cmd/issue/develop/develop.go | 98 +++++++++++-- pkg/cmd/issue/develop/develop_test.go | 189 ++++++++++++++++++++++++++ 2 files changed, 274 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/issue/develop/develop.go b/pkg/cmd/issue/develop/develop.go index 04bf14ebe..812194cf0 100644 --- a/pkg/cmd/issue/develop/develop.go +++ b/pkg/cmd/issue/develop/develop.go @@ -4,6 +4,8 @@ import ( ctx "context" "fmt" "net/http" + "net/url" + "strings" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" @@ -174,7 +176,6 @@ func developRun(opts *DevelopOptions) error { func developRunCreate(opts *DevelopOptions, apiClient *api.Client, issueRepo ghrepo.Interface, issue *api.Issue) error { branchRepo := issueRepo - var repoID string if opts.BranchRepo != "" { var err error branchRepo, err = ghrepo.FromFullName(opts.BranchRepo) @@ -183,24 +184,66 @@ func developRunCreate(opts *DevelopOptions, apiClient *api.Client, issueRepo ghr } } - opts.IO.StartProgressIndicator() - repoID, branchID, err := api.FindRepoBranchID(apiClient, branchRepo, opts.BaseBranch) - opts.IO.StopProgressIndicator() - if err != nil { - return err + branchName := "" + reusedExisting := false + if opts.Name != "" { + opts.IO.StartProgressIndicator() + branches, err := api.ListLinkedBranches(apiClient, issueRepo, issue.Number) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + branchName = findExistingLinkedBranchName(branches, branchRepo, opts.Name) + reusedExisting = branchName != "" } - opts.IO.StartProgressIndicator() - branchName, err := api.CreateLinkedBranch(apiClient, branchRepo.RepoHost(), repoID, issue.ID, branchID, opts.Name) - opts.IO.StopProgressIndicator() - if err != nil { - return err + repoID := "" + branchID := "" + baseValidated := false + if opts.BaseBranch != "" { + opts.IO.StartProgressIndicator() + foundRepoID, foundBranchID, err := api.FindRepoBranchID(apiClient, branchRepo, opts.BaseBranch) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + repoID = foundRepoID + branchID = foundBranchID + baseValidated = true + } + + if branchName == "" { + if !baseValidated { + opts.IO.StartProgressIndicator() + foundRepoID, foundBranchID, err := api.FindRepoBranchID(apiClient, branchRepo, opts.BaseBranch) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + repoID = foundRepoID + branchID = foundBranchID + } + + opts.IO.StartProgressIndicator() + createdBranchName, err := api.CreateLinkedBranch(apiClient, branchRepo.RepoHost(), repoID, issue.ID, branchID, opts.Name) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + branchName = createdBranchName + } + + if branchName == "" { + return fmt.Errorf("failed to create linked branch: API returned empty branch name") + } + + if reusedExisting && opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.ErrOut, "Using existing linked branch %q\n", branchName) } // Remember which branch to target when creating a PR. if opts.BaseBranch != "" { - err = opts.GitClient.SetBranchConfig(ctx.Background(), branchName, git.MergeBaseConfig, opts.BaseBranch) - if err != nil { + if err := opts.GitClient.SetBranchConfig(ctx.Background(), branchName, git.MergeBaseConfig, opts.BaseBranch); err != nil { return err } } @@ -210,6 +253,35 @@ func developRunCreate(opts *DevelopOptions, apiClient *api.Client, issueRepo ghr return checkoutBranch(opts, branchRepo, branchName) } +func findExistingLinkedBranchName(branches []api.LinkedBranch, branchRepo ghrepo.Interface, branchName string) string { + for _, branch := range branches { + if branch.BranchName != branchName { + continue + } + linkedRepo, err := linkedBranchRepoFromURL(branch.URL) + if err != nil { + continue + } + if ghrepo.IsSame(linkedRepo, branchRepo) { + return branch.BranchName + } + } + return "" +} + +func linkedBranchRepoFromURL(branchURL string) (ghrepo.Interface, error) { + u, err := url.Parse(branchURL) + if err != nil { + return nil, err + } + pathParts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3) + if len(pathParts) < 2 { + return nil, fmt.Errorf("invalid linked branch URL: %q", branchURL) + } + u.Path = "/" + strings.Join(pathParts[0:2], "/") + return ghrepo.FromURL(u) +} + func developRunList(opts *DevelopOptions, apiClient *api.Client, issueRepo ghrepo.Interface, issue *api.Issue) error { opts.IO.StartProgressIndicator() branches, err := api.ListLinkedBranches(apiClient, issueRepo, issue.Number) diff --git a/pkg/cmd/issue/develop/develop_test.go b/pkg/cmd/issue/develop/develop_test.go index 2485c8cc4..fe984df79 100644 --- a/pkg/cmd/issue/develop/develop_test.go +++ b/pkg/cmd/issue/develop/develop_test.go @@ -353,6 +353,16 @@ func TestDevelopRun(t *testing.T) { reg.Register( httpmock.GraphQL(`query FindRepoBranchID\b`), httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`)) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) reg.Register( httpmock.GraphQL(`mutation CreateLinkedBranch\b`), httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, @@ -370,6 +380,165 @@ func TestDevelopRun(t *testing.T) { }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", }, + { + name: "develop existing linked branch with name and checkout", + opts: &DevelopOptions{ + Name: "my-branch", + BaseBranch: "main", + IssueNumber: 123, + Checkout: true, + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"my-branch","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) + reg.Register( + httpmock.GraphQL(`query FindRepoBranchID\b`), + httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`)) + }, + runStubs: func(cs *run.CommandStubber) { + cs.Register(`git config branch\.my-branch\.gh-merge-base main`, 0, "") + cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") + cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") + cs.Register(`git checkout my-branch`, 0, "") + cs.Register(`git pull --ff-only origin my-branch`, 0, "") + }, + expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", + }, + { + name: "develop existing linked branch with name in tty shows reuse message", + opts: &DevelopOptions{ + Name: "my-branch", + BaseBranch: "main", + IssueNumber: 123, + }, + tty: true, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"my-branch","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) + reg.Register( + httpmock.GraphQL(`query FindRepoBranchID\b`), + httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`)) + }, + runStubs: func(cs *run.CommandStubber) { + cs.Register(`git config branch\.my-branch\.gh-merge-base main`, 0, "") + cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") + }, + expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", + expectedErrOut: "Using existing linked branch \"my-branch\"\n", + }, + { + name: "develop existing linked branch with invalid base branch returns an error", + opts: &DevelopOptions{ + Name: "my-branch", + BaseBranch: "does-not-exist-branch", + IssueNumber: 123, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"my-branch","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) + reg.Register( + httpmock.GraphQL(`query FindRepoBranchID\b`), + httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":null}}}`), + ) + }, + wantErr: `could not find branch "does-not-exist-branch" in OWNER/REPO`, + }, + { + name: "develop with empty linked branch name response returns an error", + opts: &DevelopOptions{ + Name: "my-branch", + BaseBranch: "main", + IssueNumber: 123, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) + reg.Register( + httpmock.GraphQL(`query FindRepoBranchID\b`), + httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`)) + reg.Register( + httpmock.GraphQL(`mutation CreateLinkedBranch\b`), + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":""}}}}}`, + func(inputs map[string]interface{}) { + assert.Equal(t, "REPOID", inputs["repositoryId"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) + assert.Equal(t, "OID", inputs["oid"]) + assert.Equal(t, "my-branch", inputs["name"]) + }), + ) + }, + wantErr: "failed to create linked branch: API returned empty branch name", + }, { name: "develop new branch outside of local git repo", opts: &DevelopOptions{ @@ -426,6 +595,16 @@ func TestDevelopRun(t *testing.T) { httpmock.GraphQL(`query FindRepoBranchID\b`), httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`), ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) reg.Register( httpmock.GraphQL(`mutation CreateLinkedBranch\b`), httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, @@ -468,6 +647,16 @@ func TestDevelopRun(t *testing.T) { httpmock.GraphQL(`query FindRepoBranchID\b`), httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`), ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(123), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + }), + ) reg.Register( httpmock.GraphQL(`mutation CreateLinkedBranch\b`), httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, From 620261fea4ebdfa4eb5b8edaa62e8cfc5668274f Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:53:30 -0700 Subject: [PATCH 019/126] Remove redundant comments --- pkg/cmd/pr/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index d29240fe5..caf79c550 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -399,7 +399,7 @@ func createRun(opts *CreateOptions) error { client := ctx.Client // Detect ActorIsAssignable feature to determine if we can use search-based - // reviewer selection (github.com) or need to use traditional ID-based selection (GHES) + // reviewer selection (github.com) or need to use legacy ID-based selection (GHES) issueFeatures, _ := opts.Detector.IssueFeatures() var reviewerSearchFunc func(string) prompter.MultiSelectSearchResult if issueFeatures.ActorIsAssignable { From dd9ab7152b8a628eb43cf36e49d3398b7fe96215 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:54:03 -0700 Subject: [PATCH 020/126] Don't swallow error from FD Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cmd/pr/create/create.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index caf79c550..bd6c79f9c 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -400,7 +400,10 @@ func createRun(opts *CreateOptions) error { // Detect ActorIsAssignable feature to determine if we can use search-based // reviewer selection (github.com) or need to use legacy ID-based selection (GHES) - issueFeatures, _ := opts.Detector.IssueFeatures() + issueFeatures, err := opts.Detector.IssueFeatures() + if err != nil { + return err + } var reviewerSearchFunc func(string) prompter.MultiSelectSearchResult if issueFeatures.ActorIsAssignable { // Create search function for reviewer selection using login-based API From 7373de3e707b59889cb186b617d3158e33db5c2d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:58:23 -0700 Subject: [PATCH 021/126] Remove redundant comment --- pkg/cmd/pr/create/create.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index bd6c79f9c..631e69430 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -406,7 +406,6 @@ func createRun(opts *CreateOptions) error { } var reviewerSearchFunc func(string) prompter.MultiSelectSearchResult if issueFeatures.ActorIsAssignable { - // Create search function for reviewer selection using login-based API reviewerSearchFunc = func(query string) prompter.MultiSelectSearchResult { candidates, moreResults, err := api.SuggestedReviewerActorsForRepo(client, ctx.PRRefs.BaseRepo(), query) if err != nil { From cf08f4d51e55cb5ced9812ba51d0303f395183dd Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:08:12 -0700 Subject: [PATCH 022/126] Apply suggestion from @BagToad --- pkg/cmd/pr/shared/survey.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index 7bcf3a4a7..ae84a6ef4 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -255,7 +255,6 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface if isChosen("Reviewers") { if reviewerSearchFunc != nil { - // Use search-based selection (github.com with ActorIsAssignable) selectedReviewers, err := p.MultiSelectWithSearch( "Reviewers", "Search reviewers", From db167d31164bc3c18d39a45f2261f79eb047a03a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:15:14 -0700 Subject: [PATCH 023/126] Preserve org/slug format for team reviewer slugs --- pkg/cmd/pr/shared/params.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 70990f2bf..c6c5661c8 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -131,17 +131,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par // RequestReviewsByLogin mutation. Otherwise, resolve to IDs for GHES compatibility. if tb.ActorReviewers { params["userReviewerLogins"] = userReviewers - // Extract team slugs from org/slug format - teamSlugs := make([]string, len(teamReviewers)) - for i, t := range teamReviewers { - parts := strings.SplitN(t, "/", 2) - if len(parts) == 2 { - teamSlugs[i] = parts[1] - } else { - teamSlugs[i] = t - } - } - params["teamReviewerSlugs"] = teamSlugs + params["teamReviewerSlugs"] = teamReviewers } else { userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) if err != nil { From 1209b24e69fe07a8ae4f291325d20a5c790c6b76 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:16:03 -0700 Subject: [PATCH 024/126] Partition bot reviewers separately for RequestReviewsByLogin --- pkg/cmd/pr/shared/params.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index c6c5661c8..90e1e6f89 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -118,10 +118,13 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par } var userReviewers []string + var botReviewers []string var teamReviewers []string for _, r := range tb.Reviewers { if strings.ContainsRune(r, '/') { teamReviewers = append(teamReviewers, r) + } else if r == api.CopilotReviewerLogin { + botReviewers = append(botReviewers, r) } else { userReviewers = append(userReviewers, r) } @@ -131,6 +134,9 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par // RequestReviewsByLogin mutation. Otherwise, resolve to IDs for GHES compatibility. if tb.ActorReviewers { params["userReviewerLogins"] = userReviewers + if len(botReviewers) > 0 { + params["botReviewerLogins"] = botReviewers + } params["teamReviewerSlugs"] = teamReviewers } else { userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers) From ad64d10bf4f59c41581b905f5479f9b561b0ef9a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:16:23 -0700 Subject: [PATCH 025/126] Wire bot reviewer logins through CreatePullRequest --- api/queries_pr.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index ccde36935..390361277 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -618,11 +618,12 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter // Request reviewers using either login-based (github.com) or ID-based (GHES) mutation userLogins, hasUserLogins := params["userReviewerLogins"].([]string) + botLogins, _ := params["botReviewerLogins"].([]string) teamSlugs, hasTeamSlugs := params["teamReviewerSlugs"].([]string) if hasUserLogins || hasTeamSlugs { // Use login-based mutation (RequestReviewsByLogin) for github.com - err := RequestReviewsByLogin(client, repo, pr.ID, userLogins, nil, teamSlugs, true) + err := RequestReviewsByLogin(client, repo, pr.ID, userLogins, botLogins, teamSlugs, true) if err != nil { return pr, err } From 38661646eef035f08e04e1c4e12777bedf10cddd Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:18:07 -0700 Subject: [PATCH 026/126] Update test assertions to expect org/slug team format --- pkg/cmd/pr/create/create_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 254b54528..2adf1007b 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -987,7 +987,7 @@ func Test_createRun(t *testing.T) { `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) assert.Equal(t, []interface{}{"hubot", "monalisa"}, inputs["userLogins"]) - assert.Equal(t, []interface{}{"core", "robots"}, inputs["teamSlugs"]) + assert.Equal(t, []interface{}{"/core", "/robots"}, inputs["teamSlugs"]) assert.Equal(t, true, inputs["union"]) })) }, @@ -1516,7 +1516,7 @@ func Test_createRun(t *testing.T) { `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) assert.Equal(t, []interface{}{"hubot", "monalisa"}, inputs["userLogins"]) - assert.Equal(t, []interface{}{"core", "robots"}, inputs["teamSlugs"]) + assert.Equal(t, []interface{}{"org/core", "org/robots"}, inputs["teamSlugs"]) assert.Equal(t, true, inputs["union"]) })) }, From e361335e5c1f67154216c80bfaeccdce29966321 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:18:39 -0700 Subject: [PATCH 027/126] Skip reviewer metadata fetch when using search-based selection --- pkg/cmd/pr/shared/survey.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index ae84a6ef4..6c88a74af 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -180,10 +180,13 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface state.Metadata = append(state.Metadata, extraFieldsOptions[i]) } - // Retrieve and process data for survey prompts based on the extra fields selected + // Retrieve and process data for survey prompts based on the extra fields selected. + // When search-based reviewer selection is available, skip the expensive assignable-users + // and teams fetch since reviewers are found dynamically via the search function. + useReviewerSearch := reviewerSearchFunc != nil metadataInput := api.RepoMetadataInput{ - Reviewers: isChosen("Reviewers"), - TeamReviewers: isChosen("Reviewers"), + Reviewers: isChosen("Reviewers") && !useReviewerSearch, + TeamReviewers: isChosen("Reviewers") && !useReviewerSearch, Assignees: isChosen("Assignees"), ActorAssignees: isChosen("Assignees") && state.ActorAssignees, Labels: isChosen("Labels"), @@ -197,13 +200,15 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface } var reviewers []string - for _, u := range metadataResult.AssignableUsers { - if u.Login() != metadataResult.CurrentLogin { - reviewers = append(reviewers, u.DisplayName()) + if !useReviewerSearch { + for _, u := range metadataResult.AssignableUsers { + if u.Login() != metadataResult.CurrentLogin { + reviewers = append(reviewers, u.DisplayName()) + } + } + for _, t := range metadataResult.Teams { + reviewers = append(reviewers, fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), t.Slug)) } - } - for _, t := range metadataResult.Teams { - reviewers = append(reviewers, fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), t.Slug)) } // Populate the list of selectable assignees and their default selections. From a8655bcda9261f462f6bc37f97b157fd1893c7c3 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:55:57 -0700 Subject: [PATCH 028/126] Include bot logins in login-based reviewer mutation guard --- api/queries_pr.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 390361277..eb2eb5494 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -618,10 +618,10 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter // Request reviewers using either login-based (github.com) or ID-based (GHES) mutation userLogins, hasUserLogins := params["userReviewerLogins"].([]string) - botLogins, _ := params["botReviewerLogins"].([]string) + botLogins, hasBotLogins := params["botReviewerLogins"].([]string) teamSlugs, hasTeamSlugs := params["teamReviewerSlugs"].([]string) - if hasUserLogins || hasTeamSlugs { + if hasUserLogins || hasBotLogins || hasTeamSlugs { // Use login-based mutation (RequestReviewsByLogin) for github.com err := RequestReviewsByLogin(client, repo, pr.ID, userLogins, botLogins, teamSlugs, true) if err != nil { From 1cb776384ecd1c2a935b86096fe58ad31b9facfb Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:33:13 -0700 Subject: [PATCH 029/126] Normalize /slug team shorthand to org/slug and fix docs --- api/queries_pr.go | 4 ++-- pkg/cmd/pr/create/create_test.go | 2 +- pkg/cmd/pr/shared/params.go | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index eb2eb5494..2ee1d5e5f 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -764,8 +764,8 @@ func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber in // RequestReviewsByLogin sets requested reviewers on a pull request using the GraphQL mutation. // This mutation replaces existing reviewers with the provided set unless union is true. // Only available on github.com, not GHES. -// Bot logins should include the [bot] suffix (e.g., "copilot-pull-request-reviewer[bot]"). -// Team slugs should be in the format "org/team-slug". +// Bot logins should be passed without the [bot] suffix; it is appended automatically. +// Team slugs must be in the format "org/team-slug". // When union is false (replace mode), passing empty slices will remove all reviewers. func RequestReviewsByLogin(client *Client, repo ghrepo.Interface, prID string, userLogins, botLogins, teamSlugs []string, union bool) error { // In union mode (additive), nothing to do if all lists are empty. diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 2adf1007b..a5e30cfcd 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -987,7 +987,7 @@ func Test_createRun(t *testing.T) { `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) assert.Equal(t, []interface{}{"hubot", "monalisa"}, inputs["userLogins"]) - assert.Equal(t, []interface{}{"/core", "/robots"}, inputs["teamSlugs"]) + assert.Equal(t, []interface{}{"OWNER/core", "OWNER/robots"}, inputs["teamSlugs"]) assert.Equal(t, true, inputs["union"]) })) }, diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 90e1e6f89..06f599f0d 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -122,6 +122,10 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par var teamReviewers []string for _, r := range tb.Reviewers { if strings.ContainsRune(r, '/') { + // Normalize /slug shorthand to org/slug using the repo owner + if strings.HasPrefix(r, "/") { + r = baseRepo.RepoOwner() + r + } teamReviewers = append(teamReviewers, r) } else if r == api.CopilotReviewerLogin { botReviewers = append(botReviewers, r) From 1d730951d2a13b3b959d939a977a3f128d883d3e Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:33:13 -0700 Subject: [PATCH 030/126] Use org/slug format in test fixtures and remove /slug normalization --- pkg/cmd/pr/create/create_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index a5e30cfcd..428aca650 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -909,7 +909,7 @@ func Test_createRun(t *testing.T) { opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} opts.Projects = []string{"roadmap"} - opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} + opts.Reviewers = []string{"hubot", "monalisa", "OWNER/core", "OWNER/robots"} opts.Milestone = "big one.oh" return func() {} }, @@ -1648,7 +1648,7 @@ func Test_createRun_GHES(t *testing.T) { opts.HeadBranch = "feature" opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} - opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} + opts.Reviewers = []string{"hubot", "monalisa", "OWNER/core", "OWNER/robots"} opts.Milestone = "big one.oh" opts.DryRun = true return func() {} @@ -1709,7 +1709,7 @@ func Test_createRun_GHES(t *testing.T) { `base: trunk`, `head: feature`, `labels: bug, todo`, - `reviewers: hubot, monalisa, /core, /robots`, + `reviewers: hubot, monalisa, OWNER/core, OWNER/robots`, `assignees: monalisa`, `milestones: big one.oh`, `maintainerCanModify: false`, @@ -1731,7 +1731,7 @@ func Test_createRun_GHES(t *testing.T) { opts.HeadBranch = "feature" opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} - opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} + opts.Reviewers = []string{"hubot", "monalisa", "OWNER/core", "OWNER/robots"} opts.Milestone = "big one.oh" opts.DryRun = true return func() {} @@ -1792,7 +1792,7 @@ func Test_createRun_GHES(t *testing.T) { `Base: trunk`, `Head: feature`, `Labels: bug, todo`, - `Reviewers: hubot, monalisa, /core, /robots`, + `Reviewers: hubot, monalisa, OWNER/core, OWNER/robots`, `Assignees: monalisa`, `Milestones: big one.oh`, `MaintainerCanModify: false`, From 7448aed8ab3427d522e72416989e2e025c58ce8e Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 12 Feb 2026 16:56:14 +0100 Subject: [PATCH 031/126] fork default branch only in pr create --- pkg/cmd/pr/create/create.go | 2 +- pkg/cmd/pr/create/create_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 5b5d45f9c..8411cdada 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -1128,7 +1128,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error { forkableRefs, requiresFork := refs.(forkableRefs) if requiresFork { opts.IO.StartProgressIndicator() - forkedRepo, err := api.ForkRepo(ctx.Client, forkableRefs.BaseRepo(), "", "", false) + forkedRepo, err := api.ForkRepo(ctx.Client, forkableRefs.BaseRepo(), "", "", true) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("error forking repo: %w", err) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 5ccffd7a8..672869025 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -894,11 +894,13 @@ func Test_createRun(t *testing.T) { httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/forks"), - httpmock.StatusStringResponse(201, ` + httpmock.RESTPayload(201, ` { "node_id": "NODEID", "name": "REPO", "owner": {"login": "monalisa"} - }`)) + }`, func(payload map[string]interface{}) { + assert.Equal(t, true, payload["default_branch_only"]) + })) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` From a5e97b5b6c98e66c862201e980051f479b04d230 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:32:45 -0700 Subject: [PATCH 032/126] Migrate issue triage workflows to shared workflows --- .github/workflows/feature-request-comment.yml | 36 --------- .github/workflows/issueauto.yml | 25 ------- .../scripts/spam-detection/eval-prompts.yml | 2 +- .github/workflows/stale-issues.yml | 2 +- .github/workflows/triage-issues.yml | 61 +++++++++++++++ .github/workflows/triage-scheduled-tasks.yml | 13 ++++ .github/workflows/triage.yml | 74 +++---------------- docs/triage.md | 14 ++-- 8 files changed, 94 insertions(+), 133 deletions(-) delete mode 100644 .github/workflows/feature-request-comment.yml delete mode 100644 .github/workflows/issueauto.yml create mode 100644 .github/workflows/triage-issues.yml create mode 100644 .github/workflows/triage-scheduled-tasks.yml diff --git a/.github/workflows/feature-request-comment.yml b/.github/workflows/feature-request-comment.yml deleted file mode 100644 index 8426d7af2..000000000 --- a/.github/workflows/feature-request-comment.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Add feature-request comment -on: - issues: - types: - - labeled - -permissions: - issues: write - -jobs: - add-comment-to-feature-request-issues: - if: github.event.label.name == 'enhancement' - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - BODY: > - Thank you for your issue! We have categorized it as a feature request, - and it has been added to our backlog. In doing so, **we are not - committing to implementing this feature at this time**, but, we will - consider it for future releases based on community feedback and our own - product roadmap. - - - Unless you see the - https://github.com/cli/cli/labels/help%20wanted label, we are - not currently looking for external contributions for this feature. - - - **If you come across this issue and would like to see it implemented, - please add a thumbs up!** This will help us prioritize the feature. - Please only comment if you have additional information or viewpoints to - contribute. - steps: - - run: gh issue comment "$NUMBER" --body "$BODY" diff --git a/.github/workflows/issueauto.yml b/.github/workflows/issueauto.yml deleted file mode 100644 index cfdcff764..000000000 --- a/.github/workflows/issueauto.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Issue Automation -on: - issues: - types: [opened] - -permissions: - contents: none - issues: write - -jobs: - issue-auto: - runs-on: ubuntu-latest - environment: cli-automation - steps: - - name: label incoming issue - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} - ISSUENUM: ${{ github.event.issue.number }} - ISSUEAUTHOR: ${{ github.event.issue.user.login }} - run: | - if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null - then - gh issue edit $ISSUENUM --add-label "needs-triage" - fi \ No newline at end of file diff --git a/.github/workflows/scripts/spam-detection/eval-prompts.yml b/.github/workflows/scripts/spam-detection/eval-prompts.yml index 15c61ff76..691101388 100644 --- a/.github/workflows/scripts/spam-detection/eval-prompts.yml +++ b/.github/workflows/scripts/spam-detection/eval-prompts.yml @@ -918,7 +918,7 @@ testData: We have an automation to nudge on issues waiting for user info (like after one week), and close the issue if there's no further activity (like after one more week). - - Automatically add the stale label to issues labelled needs-user-input after 30 days of inactivity. When the stale label is added, also post a comment to the issue explaining what this means: the issue will close after 30 days of inactivity; contributors can comment on the issue to remove the stale label and keep it open. Maintainers can also add the keep label to make the stale automation ignore that issue. + - Automatically add the stale label to issues labelled more-info-needed after 30 days of inactivity. When the stale label is added, also post a comment to the issue explaining what this means: the issue will close after 30 days of inactivity; contributors can comment on the issue to remove the stale label and keep it open. Maintainers can also add the keep label to make the stale automation ignore that issue. - Automatically close issues labelled stale after they have been stale for 30 days. When the issue is closed, add a comment explaining why this happened. Encourage them to leave a comment if the close was done in error. - The above automation should only act on new issues after the date of the automation's implementation. diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 543a909c8..d4c5967aa 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -15,7 +15,7 @@ jobs: start-date: "2025-07-10T00:00:00Z" # Skip for issues created before this date days-before-issue-stale: 30 only-issue-labels: - "needs-triage,needs-user-input" # Only issues with all of these labels can be marked as stale + "needs-triage,more-info-needed" # Only issues with all of these labels can be marked as stale exempt-issue-labels: "keep" # Issues marked with this label should not be marked as stale stale-issue-label: "stale" # Mark stale issues with this label stale-issue-message: | diff --git a/.github/workflows/triage-issues.yml b/.github/workflows/triage-issues.yml new file mode 100644 index 000000000..199952ee2 --- /dev/null +++ b/.github/workflows/triage-issues.yml @@ -0,0 +1,61 @@ +name: Issue Triaging +on: + issues: + types: [opened, reopened, labeled, unlabeled, closed] + +jobs: + label-incoming: + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'unlabeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-label-incoming.yml@main + permissions: + issues: write + + close-invalid: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-invalid.yml@main + permissions: + contents: read + issues: write + pull-requests: write + + close-suspected-spam: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-suspected-spam.yml@main + permissions: + issues: write + + close-single-word: + if: github.event.action == 'opened' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-single-word-issues.yml@main + permissions: + issues: write + + close-off-topic: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-close-off-topic.yml@main + permissions: + issues: write + + enhancement-comment: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-enhancement-comment.yml@main + permissions: + issues: write + + unable-to-reproduce: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-unable-to-reproduce-comment.yml@main + permissions: + issues: write + + remove-needs-triage: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-remove-needs-triage.yml@main + permissions: + issues: write + + on-issue-close: + if: github.event.action == 'closed' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-on-issue-close.yml@main + permissions: + issues: write diff --git a/.github/workflows/triage-scheduled-tasks.yml b/.github/workflows/triage-scheduled-tasks.yml new file mode 100644 index 000000000..721e899f3 --- /dev/null +++ b/.github/workflows/triage-scheduled-tasks.yml @@ -0,0 +1,13 @@ +name: Triage Scheduled Tasks +on: + workflow_dispatch: + issue_comment: + types: [created] + schedule: + - cron: '5 * * * *' # Hourly — no-response close + +jobs: + no-response: + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-no-response-close.yml@main + permissions: + issues: write diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index a2ca17160..20eca49c8 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -5,70 +5,18 @@ on: issues: types: - labeled + # pull_request_target (not pull_request) to access secrets for fork PRs. + # Safe: no PR code is checked out or executed. pull_request_target: types: - labeled -env: - TARGET_REPO: github/cli + jobs: - issue: - environment: cli-discuss-automation - runs-on: ubuntu-latest - if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'discuss' - steps: - - name: Create issue based on source issue - env: - BODY: ${{ github.event.issue.body }} - CREATED: ${{ github.event.issue.created_at }} - GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} - LINK: ${{ github.repository }}#${{ github.event.issue.number }} - TITLE: ${{ github.event.issue.title }} - TRIGGERED_BY: ${{ github.triggering_actor }} - run: | - # Markdown quote source body by replacing newlines for newlines and markdown quoting - BODY="${BODY//$'\n'/$'\n'> }" - - # Create issue using dynamically constructed body within heredoc - cat << EOF | gh issue create --title "Triage issue \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage - **Title:** $TITLE - **Issue:** $LINK - **Created:** $CREATED - **Triggered by:** @$TRIGGERED_BY - - --- - - cc: @github/cli - - > $BODY - EOF - - pull_request: - runs-on: ubuntu-latest - environment: cli-discuss-automation - if: github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'discuss' - steps: - - name: Create issue based on source pull request - env: - BODY: ${{ github.event.pull_request.body }} - CREATED: ${{ github.event.pull_request.created_at }} - GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} - LINK: ${{ github.repository }}#${{ github.event.pull_request.number }} - TITLE: ${{ github.event.pull_request.title }} - TRIGGERED_BY: ${{ github.triggering_actor }} - run: | - # Markdown quote source body by replacing newlines for newlines and markdown quoting - BODY="${BODY//$'\n'/$'\n'> }" - - # Create issue using dynamically constructed body within heredoc - cat << EOF | gh issue create --title "Triage PR \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage - **Title:** $TITLE - **Pull request:** $LINK - **Created:** $CREATED - **Triggered by:** @$TRIGGERED_BY - - --- - - cc: @github/cli - - > $BODY - EOF + discuss: + if: github.event.action == 'labeled' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-discuss.yml@main + with: + target_repo: 'github/cli' + cc_team: '@github/cli' + secrets: + discussion_token: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} diff --git a/docs/triage.md b/docs/triage.md index ea119994c..54443345f 100644 --- a/docs/triage.md +++ b/docs/triage.md @@ -14,16 +14,16 @@ For bugs, the FR should engage with the issue and community with the goal to rem To be considered triaged, `bug` issues require the following: -- A severity label `p1`, `p2`, and `p3` +- A severity label `priority-1`, `priority-2`, and `priority-3` - Clearly defined Acceptance Criteria, added to the Issue as a standalone comment (see [example](https://github.com/cli/cli/issues/9469#issuecomment-2292315743)) #### Bug severities | Severity | Description | | - | - | -| `p1` | Affects a large population and inhibits work | -| `p2` | Affects more than a few users but doesn't prevent core functions | -| `p3` | Affects a small number of users or is largely cosmetic | +| `priority-1` | Affects a large population and inhibits work | +| `priority-2` | Affects more than a few users but doesn't prevent core functions | +| `priority-3` | Affects a small number of users or is largely cosmetic | ### Enhancements and Docs @@ -36,10 +36,10 @@ When a new issue is opened, the FR **should**: - Ensure there is enough information to understand the enhancement's scope and value - Ask the user for more information about value and use-case, if necessary - Leave the `needs-triage` label on the issue -- Add the `needs-user-input` and `needs-investigation` labels as needed +- Add the `more-info-needed` and `needs-investigation` labels as needed When the FR has enough information to be triaged, they should: -- Remove the `needs-user-input` and `needs-investigation` labels +- Remove the `more-info-needed` and `needs-investigation` labels - Remove their assignment from the issue The FR should **avoid**: @@ -57,7 +57,7 @@ The FR can consider adding any of the following labels below. | - | - | | `discuss` | Some issues require discussion with the internal team. Adding this label will automatically open up an internal discussion with the team to facilitate this discussion. | | `core` | Defines what we would like to do internally. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. | -| `needs-user-input` | After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. | +| `more-info-needed` | After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. | | `needs-investigation` | Used when the issue requires further investigation before it can be reviewed and triaged. This is often used for issues that are not clearly bugs or enhancements, or when the FR needs to gather more information before proceeding. | | `invalid` | Added to spam and abusive issues. | From 2b5c3b5ecb8f9c6673c6996aeedc9420706ce92d Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Feb 2026 15:36:37 +0100 Subject: [PATCH 033/126] document fork default branch behavior --- pkg/cmd/pr/create/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 8411cdada..3949ecbb6 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -214,7 +214,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co Upon success, the URL of the created pull request will be printed. When the current branch isn't fully pushed to a git remote, a prompt will ask where - to push the branch and offer an option to fork the base repository. Use %[1]s--head%[1]s to + to push the branch and offer an option to fork the base repository. Any fork created this + way will only have the default branch of the upstream repository. Use %[1]s--head%[1]s to explicitly skip any forking or pushing behavior. %[1]s--head%[1]s supports %[1]s:%[1]s syntax to select a head repo owned by %[1]s%[1]s. From 24964681a8ab84470e623017d066c2815e1692d8 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Feb 2026 15:34:20 +0100 Subject: [PATCH 034/126] Respect --exit-status with --log and --log-failed Fixes #12674 --- pkg/cmd/run/view/view.go | 9 +++++- pkg/cmd/run/view/view_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index d50b14fdf..bed9e3bfa 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -331,7 +331,14 @@ func runView(opts *ViewOptions) error { return err } - return displayLogSegments(opts.IO.Out, segments) + if err := displayLogSegments(opts.IO.Out, segments); err != nil { + return err + } + + if opts.ExitStatus && shared.IsFailureState(run.Conclusion) { + return cmdutil.SilentError + } + return nil } prNumber := "" diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index 5bcb587d1..14749fcf6 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -1048,6 +1048,64 @@ func TestViewRun(t *testing.T) { }, wantOut: quuxTheBarfLogOutput, }, + { + name: "exit status respected with log-failed, failed run", + opts: &ViewOptions{ + RunID: "1234", + LogFailed: true, + ExitStatus: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + shared.FailedJob, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.BinaryResponse(zipArchive)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: quuxTheBarfLogOutput, + wantErr: true, + }, + { + name: "exit status respected with log, failed run", + opts: &ViewOptions{ + RunID: "1234", + Log: true, + ExitStatus: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + shared.FailedJob, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), + httpmock.BinaryResponse(zipArchive)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + }, + wantOut: expectedRunLogOutput, + wantErr: true, + }, { name: "interactive with log, with no step logs available (#10551)", tty: true, From f1ebf6f8d99b2e84188b97062040a080a1cf471b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:17:33 -0700 Subject: [PATCH 035/126] Migrate stale workflow to shared workflow --- .github/workflows/stale-issues.yml | 36 -------------------- .github/workflows/triage-scheduled-tasks.yml | 13 +++++++ 2 files changed, 13 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/stale-issues.yml diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml deleted file mode 100644 index d4c5967aa..000000000 --- a/.github/workflows/stale-issues.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Marks/closes stale issues -on: - schedule: - - cron: "0 3 * * *" # 3 AM UTC - -permissions: - issues: write - -jobs: - mark-stale-issues: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v10 - with: - start-date: "2025-07-10T00:00:00Z" # Skip for issues created before this date - days-before-issue-stale: 30 - only-issue-labels: - "needs-triage,more-info-needed" # Only issues with all of these labels can be marked as stale - exempt-issue-labels: "keep" # Issues marked with this label should not be marked as stale - stale-issue-label: "stale" # Mark stale issues with this label - stale-issue-message: | - This issue has been automatically marked as stale because it has not had any activity in the last 30 days, - and it will be closed in 30 days if no further activity occurs. - - If you think this is a mistake, please comment on this issue to keep it open. - - days-before-issue-close: 30 - close-issue-reason: "not_planned" - close-issue-message: | - This issue has been automatically closed due to inactivity. - - If you think this is a mistake, please comment on this issue. - - # Exclude PRs from closing or being marked as stale - days-before-pr-stale: -1 - days-before-pr-close: -1 diff --git a/.github/workflows/triage-scheduled-tasks.yml b/.github/workflows/triage-scheduled-tasks.yml index 721e899f3..8dd0793b2 100644 --- a/.github/workflows/triage-scheduled-tasks.yml +++ b/.github/workflows/triage-scheduled-tasks.yml @@ -5,9 +5,22 @@ on: types: [created] schedule: - cron: '5 * * * *' # Hourly — no-response close + - cron: '0 3 * * *' # Daily at 3 AM UTC — stale issues jobs: no-response: uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-no-response-close.yml@main permissions: issues: write + + stale: + if: github.event.schedule == '0 3 * * *' + uses: desktop/gh-cli-and-desktop-shared-workflows/.github/workflows/triage-stale-issues.yml@main + with: + days_before_stale: 30 + days_before_close: -1 + start_date: '2025-07-10T00:00:00Z' + stale_issue_label: 'stale' + exempt_issue_labels: 'keep' + permissions: + issues: write From 166db75d365d6685e8c165d620a1b59fd5652a1b Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Feb 2026 19:42:44 +0100 Subject: [PATCH 036/126] pin REST API version to 2022-11-28 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/client.go | 3 +++ api/client_test.go | 9 ++++--- api/http_client.go | 3 ++- api/http_client_test.go | 46 +++++++++++++++++++-------------- pkg/cmd/factory/default_test.go | 1 + 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/api/client.go b/api/client.go index e6ff59c59..207fd86d3 100644 --- a/api/client.go +++ b/api/client.go @@ -16,6 +16,8 @@ import ( const ( accept = "Accept" + apiVersion = "X-GitHub-Api-Version" + apiVersionValue = "2022-11-28" authorization = "Authorization" cacheTTL = "X-GH-CACHE-TTL" graphqlFeatures = "GraphQL-Features" @@ -264,6 +266,7 @@ func clientOptions(hostname string, transport http.RoundTripper) ghAPI.ClientOpt AuthToken: "none", Headers: map[string]string{ authorization: "", + apiVersion: apiVersionValue, }, Host: hostname, SkipDefaultHeaders: true, diff --git a/api/client_test.go b/api/client_test.go index 1701a17a9..f988e090c 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -245,10 +245,11 @@ func TestHTTPHeaders(t *testing.T) { assert.NoError(t, err) wantHeader := map[string]string{ - "Accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", - "Authorization": "token MYTOKEN", - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "GitHub CLI v1.2.3", + "Accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + "Authorization": "token MYTOKEN", + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "GitHub CLI v1.2.3", + "X-GitHub-Api-Version": "2022-11-28", } for name, value := range wantHeader { assert.Equal(t, value, gotReq.Header.Get(name), name) diff --git a/api/http_client.go b/api/http_client.go index ab7d49063..9957f6bc5 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -49,7 +49,8 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) { } headers := map[string]string{ - userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), + userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), + apiVersion: apiVersionValue, } clientOpts.Headers = headers diff --git a/api/http_client_test.go b/api/http_client_test.go index 9a915837f..824bc0f1b 100644 --- a/api/http_client_test.go +++ b/api/http_client_test.go @@ -39,9 +39,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": {"token MYTOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -53,9 +54,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "example.com", wantHeader: map[string][]string{ - "authorization": {"token GHETOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token GHETOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -68,9 +70,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": nil, // should not be set - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -83,9 +86,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "example.com", wantHeader: map[string][]string{ - "authorization": nil, // should not be set - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": nil, // should not be set + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: "", }, @@ -98,9 +102,10 @@ func TestNewHTTPClient(t *testing.T) { }, host: "github.com", wantHeader: map[string][]string{ - "authorization": {"token MYTOKEN"}, - "user-agent": {"GitHub CLI v1.2.3"}, - "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, + "authorization": {"token MYTOKEN"}, + "user-agent": {"GitHub CLI v1.2.3"}, + "x-github-api-version": {"2022-11-28"}, + "accept": {"application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview"}, }, wantStderr: heredoc.Doc(` * Request at