From d45acae60445ada9d2fd632d6b9a3b78d805ba72 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 12 Mar 2026 12:45:48 +0100 Subject: [PATCH 1/2] Revert "refactor: deduplicate scope error handling between api/client.go and project queries" --- api/client.go | 1 + pkg/cmd/project/shared/queries/queries.go | 37 +++++++++++++++++-- .../project/shared/queries/queries_test.go | 32 ++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/api/client.go b/api/client.go index 2eb3f3ff20d..895f2969272 100644 --- a/api/client.go +++ b/api/client.go @@ -203,6 +203,7 @@ func GenerateScopeErrorForGQL(gqlErr *ghAPI.GraphQLError) error { } 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", diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index 1de42a4bd56..9a3bd490902 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -5,11 +5,13 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/set" "github.com/shurcooL/githubv4" ) @@ -1662,15 +1664,42 @@ func (c *Client) UnlinkProjectFromTeam(projectID string, teamID string) error { } func handleError(err error) error { - var gqlErr api.GraphQLError - if errors.As(err, &gqlErr) { - if scopeErr := api.GenerateScopeErrorForGQL(gqlErr.GraphQLError); scopeErr != nil { - return scopeErr + var gerr api.GraphQLError + if errors.As(err, &gerr) { + missing := set.NewStringSet() + for _, e := range gerr.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 err } +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 +} + func projectFieldValueData(v FieldValueNodes) interface{} { switch v.Type { case "ProjectV2ItemFieldDateValue": diff --git a/pkg/cmd/project/shared/queries/queries_test.go b/pkg/cmd/project/shared/queries/queries_test.go index dea5d13bb8f..cc4850d8620 100644 --- a/pkg/cmd/project/shared/queries/queries_test.go +++ b/pkg/cmd/project/shared/queries/queries_test.go @@ -3,6 +3,7 @@ package queries import ( "io" "net/http" + "reflect" "strings" "testing" @@ -563,6 +564,37 @@ func TestProjectFields_NoLimit(t *testing.T) { assert.Len(t, project.Fields.Nodes, 3) } +func Test_requiredScopesFromServerMessage(t *testing.T) { + tests := []struct { + name string + msg string + want []string + }{ + { + name: "no scopes", + msg: "SERVER OOPSIE", + want: []string(nil), + }, + { + name: "one scope", + msg: "Your token has not been granted the required scopes to execute this query. The 'dataType' field requires one of the following scopes: ['read:project'], but your token has only been granted the: ['codespace', repo'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.", + want: []string{"read:project"}, + }, + { + name: "multiple scopes", + msg: "Your token has not been granted the required scopes to execute this query. The 'dataType' field requires one of the following scopes: ['read:project', 'read:discussion', 'codespace'], but your token has only been granted the: [repo'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.", + want: []string{"read:project", "read:discussion", "codespace"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := requiredScopesFromServerMessage(tt.msg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("requiredScopesFromServerMessage() = %v, want %v", got, tt.want) + } + }) + } +} + func TestNewProject_nonTTY(t *testing.T) { client := NewTestClient() _, err := client.NewProject(false, &Owner{}, 0, false) From 3921788f76ee8aea4039a277c35c7aeaa64f3953 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 12 Mar 2026 12:55:55 +0100 Subject: [PATCH 2/2] Revert "fix: clarify scope error while creating issues for projects" --- api/client.go | 39 ----------------------- api/client_test.go | 77 ---------------------------------------------- 2 files changed, 116 deletions(-) diff --git a/api/client.go b/api/client.go index 895f2969272..207fd86d368 100644 --- a/api/client.go +++ b/api/client.go @@ -10,7 +10,6 @@ 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" ) @@ -181,10 +180,6 @@ 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, } @@ -193,40 +188,6 @@ 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 { diff --git a/api/client_test.go b/api/client_test.go index ad75c18896f..f988e090c3a 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -10,7 +10,6 @@ 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" ) @@ -257,79 +256,3 @@ 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) - }) - } -}