From d45acae60445ada9d2fd632d6b9a3b78d805ba72 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 12 Mar 2026 12:45:48 +0100 Subject: [PATCH] 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 2eb3f3ff2..895f29692 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 1de42a4bd..9a3bd4909 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 dea5d13bb..cc4850d86 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)