Ignore FORBIDDEN errors for gh status

Fixes #5587
This commit is contained in:
Heath Stewart 2022-05-08 12:32:41 -07:00 committed by Mislav Marohnić
parent 78ae6f85c6
commit fc4cd12cbf
3 changed files with 293 additions and 7 deletions

View file

@ -0,0 +1,104 @@
{
"data": {
"assignments": {
"edges": [
{
"node": {
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
}
},
null,
{
"node": {
"__typename": "Issue",
"updatedAt": "2022-03-10T21:33:57Z",
"title": "yolo",
"number": 157,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
}
},
null
]
},
"reviewRequested": {
"edges": [
{
"node": {
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
}
},
null
]
}
},
"errors": [
{
"type": "FORBIDDEN",
"path": [
"assignments",
"edges",
0
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
},
{
"type": "FORBIDDEN",
"path": [
"assignments",
"edges",
0
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
},
{
"type": "FORBIDDEN",
"path": [
"reviewRequested",
"edges",
0
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
}
]
}

View file

@ -8,6 +8,7 @@ import (
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/MakeNowJust/heredoc"
@ -44,7 +45,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
cmd := &cobra.Command{
Use: "status",
Short: "Print information about relevant issues, pull requests, and notifications across repositories",
Long: heredoc.Doc(`
Long: heredoc.Docf(`
The status command prints information about your work on GitHub across all the repositories you're subscribed to, including:
- Assigned Issues
@ -52,7 +53,9 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
- Review Requests
- Mentions
- Repository Activity (new issues/pull requests, comments)
`),
Some status may be unavailable due to organization SAML enforcement. Run %[1]sgh auth refresh%[1]s to authorization access to organizations.
`, "`"),
Example: heredoc.Doc(`
$ gh status -e cli/cli -e cli/go-gh # Exclude multiple repositories
$ gh status -o cli # Limit results to a single organization
@ -166,6 +169,8 @@ type StatusGetter struct {
Mentions []StatusItem
ReviewRequests []StatusItem
RepoActivity []StatusItem
hasAuthErrors bool
authErrorsMu sync.Mutex
}
func NewStatusGetter(client *http.Client, hostname string, opts *StatusOptions) *StatusGetter {
@ -295,7 +300,14 @@ func (s *StatusGetter) LoadNotifications() error {
preview: actual,
})
} else if err != nil {
return fmt.Errorf("could not fetch comment: %w", err)
var httpErr api.HTTPError
if !errors.As(err, &httpErr) || httpErr.StatusCode != 403 {
return fmt.Errorf("could not fetch comment: %w", err)
} else if httpErr.StatusCode == 403 {
s.authErrorsMu.Lock()
s.hasAuthErrors = true
s.authErrorsMu.Unlock()
}
}
}
@ -372,7 +384,18 @@ func (s *StatusGetter) LoadSearchResults() error {
}
err := c.GraphQL(s.hostname(), searchQuery, variables, &resp)
if err != nil {
return fmt.Errorf("could not search for assignments: %w", err)
// Exclude any FORBIDDEN errors and show status for what we can.
if gqlErrResponse, ok := err.(*api.GraphQLErrorResponse); ok {
err = filterGraphQLErrors(gqlErrResponse)
if err == nil {
s.authErrorsMu.Lock()
s.hasAuthErrors = true
s.authErrorsMu.Unlock()
}
}
if err != nil {
return fmt.Errorf("could not search for assignments: %w", err)
}
}
prs := []SearchResult{}
@ -384,13 +407,16 @@ func (s *StatusGetter) LoadSearchResults() error {
issues = append(issues, e.Node)
} else if e.Node.Type == "PullRequest" {
prs = append(prs, e.Node)
} else {
} else if e.Node.Type != "" {
// FORBIDDEN nodes are null in response, so Type == "" here.
panic("you shouldn't be here")
}
}
for _, e := range resp.ReviewRequested.Edges {
reviewRequested = append(reviewRequested, e.Node)
if e.Node.Type != "" {
reviewRequested = append(reviewRequested, e.Node)
}
}
sort.Sort(Results(issues))
@ -428,6 +454,19 @@ func (s *StatusGetter) LoadSearchResults() error {
return nil
}
func filterGraphQLErrors(gqlErrResponse *api.GraphQLErrorResponse) error {
gqlErrors := make([]api.GraphQLError, 0, len(gqlErrResponse.Errors))
for _, gqlErr := range gqlErrResponse.Errors {
if gqlErr.Type != "FORBIDDEN" {
gqlErrors = append(gqlErrors, gqlErr)
}
}
if len(gqlErrors) > 0 {
return &api.GraphQLErrorResponse{Errors: gqlErrors}
}
return nil
}
// Populate .RepoActivity
func (s *StatusGetter) LoadEvents() error {
perPage := 100
@ -506,6 +545,13 @@ func (s *StatusGetter) LoadEvents() error {
return nil
}
func (s *StatusGetter) HasAuthErrors() bool {
s.authErrorsMu.Lock()
defer s.authErrorsMu.Unlock()
return s.hasAuthErrors
}
func statusRun(opts *StatusOptions) error {
client, err := opts.HttpClient()
if err != nil {
@ -548,10 +594,10 @@ func statusRun(opts *StatusOptions) error {
})
err = g.Wait()
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
opts.IO.StopProgressIndicator()
cs := opts.IO.ColorScheme()
out := opts.IO.Out
@ -628,5 +674,9 @@ func statusRun(opts *StatusOptions) error {
fmt.Fprintln(out, lipgloss.JoinHorizontal(lipgloss.Top, rrSection, mSection))
fmt.Fprintln(out, raSection)
if sg.HasAuthErrors() {
fmt.Fprintln(out, cs.Gray("Some status unavailable due to organization SAML enforcement. Run `gh auth refresh` to authorization access to organizations."))
}
return nil
}

View file

@ -271,6 +271,138 @@ func TestStatusRun(t *testing.T) {
},
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ rpd/todo#110 hello @jillvalentine ...\nvilmibm/testing#1234 Foobar │ \nrpd/todo#50 Welcome party...│ \ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on Windows where it is needed\n\n",
},
{
name: "forbidden errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
httpmock.StatusStringResponse(403, `{
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
httpmock.StringResponse(`{"body":"this is a comment"}`))
reg.Register(
httpmock.REST("GET", "repos/cli/cli/issues/1096"),
httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
httpmock.StringResponse(`{"body":"not a real mention"}`))
reg.Register(
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search_forbidden.json"))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.FileResponse("./fixtures/notifications.json"))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.FileResponse("./fixtures/events.json"))
},
opts: &StatusOptions{},
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ cli/cli#1096 @jillval...\n │ vilmibm/gh-screensaver#15 a messag...\n \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on W...\nvilmibm/testing#5325 comment on Ability to sea... We are working on dedicat...\ncli/cli#5319 comment on [Codespaces] D... Wondering if we shouldn't...\ncli/cli#5300 new issue Terminal bell when a runn...\n\nSome status unavailable due to organization SAML enforcement. Run `gh auth refresh` to authorization access to organizations.\n",
},
{
name: "notification errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
httpmock.StatusStringResponse(429, `{
"message": "Too many requests."
}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.StringResponse(`{"data": { "assignments": {"edges": [] }, "reviewRequested": {"edges": []}}}`))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.StringResponse(`[
{
"reason": "mention",
"subject": {
"title": "Good",
"url": "https://api.github.com/repos/rpd/todo/issues/110",
"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/110",
"type": "Issue"
},
"repository": {
"full_name": "rpd/todo",
"owner": {
"login": "rpd"
}
}
}
]`))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.StringResponse(`[]`))
},
opts: &StatusOptions{},
wantErrMsg: "could not load notifications: could not fetch comment: HTTP 429 (https://api.github.com/repos/rpd/todo/issues/110)",
},
{
name: "search errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.StringResponse(`{
"data": {
"assignments": {
"edges": [
null
]
}
},
"errors": [
{
"type": "NOT FOUND",
"path": [
"assignments",
"edges",
0
],
"message": "Not found."
}
]
}`))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.StringResponse(`[]`))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.StringResponse(`[]`))
},
opts: &StatusOptions{},
wantErrMsg: "failed to search: could not search for assignments: GraphQL: Not found. (assignments.edges.0)",
},
}
for _, tt := range tests {