Repository: cli/cli
Pull request #7618 (2023-06-23 03:04:50)

Don't deduplicate checks that stem from different events
This PR changes `pr checks` to take into account the workflow run event when deciding if a check is duplicated. Additionally, I updated the output of `pr checks` to match what is shown on the web. I did not change the non-tty output as I was unsure if that was necessary or helpful even.

Before:

<img width="826" alt="Screenshot 2023-06-26 at 10 13 16 AM" src="https://github.com/cli/cli/assets/7969779/5f4f5947-c5d8-47c6-891c-14dfa4acd407">

After:

<img width="987" alt="Screenshot 2023-06-26 at 10 13 26 AM" src="https://github.com/cli/cli/assets/7969779/ddaee922-0f18-46f4-ae28-78fa41a34c9b">

Web:

<img width="857" alt="Screenshot 2023-06-26 at 10 13 41 AM" src="https://github.com/cli/cli/assets/7969779/3ae77ba9-542d-4cae-86dc-5cb25e0b1332">


Closes https://github.com/cli/cli/issues/7604


Patch:
diff --git a/api/queries_pr.go b/api/queries_pr.go
index eb41c4c14..982e79d62 100644
--- a/api/queries_pr.go
+++ b/api/queries_pr.go
@@ -183,16 +183,10 @@ type CheckContexts struct {
 }
 
 type CheckContext struct {
-	TypeName   string `json:"__typename"`
-	Name       string `json:"name"`
-	IsRequired bool   `json:"isRequired"`
-	CheckSuite struct {
-		WorkflowRun struct {
-			Workflow struct {
-				Name string `json:"name"`
-			} `json:"workflow"`
-		} `json:"workflowRun"`
-	} `json:"checkSuite"`
+	TypeName   string     `json:"__typename"`
+	Name       string     `json:"name"`
+	IsRequired bool       `json:"isRequired"`
+	CheckSuite CheckSuite `json:"checkSuite"`
 	// QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED
 	Status string `json:"status"`
 	// ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE
@@ -210,6 +204,19 @@ type CheckContext struct {
 	CreatedAt time.Time   `json:"createdAt"`
 }
 
+type CheckSuite struct {
+	WorkflowRun WorkflowRun `json:"workflowRun"`
+}
+
+type WorkflowRun struct {
+	Event    string   `json:"event"`
+	Workflow Workflow `json:"workflow"`
+}
+
+type Workflow struct {
+	Name string `json:"name"`
+}
+
 type PRRepository struct {
 	ID   string `json:"id"`
 	Name string `json:"name"`
diff --git a/api/query_builder.go b/api/query_builder.go
index 76eb5294a..47b2cdfc9 100644
--- a/api/query_builder.go
+++ b/api/query_builder.go
@@ -226,7 +226,7 @@ func RequiredStatusCheckRollupGraphQL(prID, after string) string {
 							},
 							...on CheckRun {
 								name,
-								checkSuite{workflowRun{workflow{name}}},
+								checkSuite{workflowRun{event,workflow{name}}},
 								status,
 								conclusion,
 								startedAt,
diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go
index fc068440c..c00b9a025 100644
--- a/pkg/cmd/auth/refresh/refresh.go
+++ b/pkg/cmd/auth/refresh/refresh.go
@@ -24,9 +24,11 @@ type RefreshOptions struct {
 
 	MainExecutable string
 
-	Hostname string
-	Scopes   []string
-	AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
+	Hostname     string
+	Scopes       []string
+	RemoveScopes []string
+	ResetScopes  bool
+	AuthFlow     func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
 
 	Interactive     bool
 	InsecureStorage bool
@@ -62,9 +64,12 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
 			your gh credentials to have. If no scopes are provided, the command
 			maintains previously added scopes.
 
-			The command can only add additional scopes, but not remove previously
-			added ones. To reset scopes to the default minimum set of scopes, you
-			will need to create new credentials using the auth login command.
+			The --remove-scope flag accepts a comma separated list of scopes you
+			want to remove from your gh credentials. Scope removal is idempotent.
+			The minimum set of scopes ("repo", "read:org" and "gist") cannot be removed.
+
+			The --reset-scopes flag resets the scopes for your gh credentials to
+			the default set of scopes for your auth flow.
 		`),
 		Example: heredoc.Doc(`
 			$ gh auth refresh --scopes write:org,read:public_key
@@ -72,6 +77,12 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
 
 			$ gh auth refresh
 			# => open a browser to ensure your authentication credentials have the correct minimum scopes
+
+			$ gh auth refresh --remove-scope delete_repo
+			# => open a browser to idempotently remove the delete_repo scope
+
+			$ gh auth refresh --reset-scopes
+			# => open a browser to re-authenticate with the default minimum scopes
 		`),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			opts.Interactive = opts.IO.CanPrompt()
@@ -90,6 +101,8 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
 
 	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication")
 	cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
+	cmd.Flags().StringSliceVarP(&opts.RemoveScopes, "remove-scope", "r", nil, "The authentication scope to remove from gh")
+	cmd.Flags().BoolVarP(&opts.ResetScopes, "reset-scopes", "R", false, "Reset authentication scopes to the default minimum set of scopes")
 	// secure storage became the default on 2023/4/04; this flag is left as a no-op for backwards compatibility
 	var secureStorage bool
 	cmd.Flags().BoolVar(&secureStorage, "secure-storage", false, "Save authentication credentials in secure credential store")
@@ -144,12 +157,14 @@ func refreshRun(opts *RefreshOptions) error {
 	}
 
 	var additionalScopes []string
-	if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
-		if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
-			for _, s := range strings.Split(oldScopes, ",") {
-				s = strings.TrimSpace(s)
-				if s != "" {
-					additionalScopes = append(additionalScopes, s)
+	if !opts.ResetScopes {
+		if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
+			if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
+				for _, s := range strings.Split(oldScopes, ",") {
+					s = strings.TrimSpace(s)
+					if s != "" {
+						additionalScopes = append(additionalScopes, s)
+					}
 				}
 			}
 		}
@@ -168,7 +183,24 @@ func refreshRun(opts *RefreshOptions) error {
 		additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
 	}
 
-	if err := opts.AuthFlow(authCfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive, !opts.InsecureStorage); err != nil {
+	additionalScopes = append(opts.Scopes, additionalScopes...)
+	if len(opts.RemoveScopes) > 0 {
+		var scopes []string
+		for _, scope := range additionalScopes {
+			var match bool
+			for _, rmScope := range opts.RemoveScopes {
+				if rmScope == scope {
+					match = true
+					break
+				}
+			}
+			if !match {
+				scopes = append(scopes, scope)
+			}
+		}
+		additionalScopes = scopes
+	}
+	if err := opts.AuthFlow(authCfg, opts.IO, hostname, additionalScopes, opts.Interactive, !opts.InsecureStorage); err != nil {
 		return err
 	}
 
diff --git a/pkg/cmd/pr/checks/aggregate.go b/pkg/cmd/pr/checks/aggregate.go
index aa139323f..f6dd036ad 100644
--- a/pkg/cmd/pr/checks/aggregate.go
+++ b/pkg/cmd/pr/checks/aggregate.go
@@ -15,6 +15,8 @@ type check struct {
 	CompletedAt time.Time `json:"completedAt"`
 	Link        string    `json:"link"`
 	Bucket      string    `json:"bucket"`
+	Event       string    `json:"event"`
+	Workflow    string    `json:"workflow"`
 }
 
 type checkCounts struct {
@@ -55,7 +57,10 @@ func aggregateChecks(checkContexts []api.CheckContext, requiredChecks bool) (che
 			StartedAt:   c.StartedAt,
 			CompletedAt: c.CompletedAt,
 			Link:        link,
+			Event:       c.CheckSuite.WorkflowRun.Event,
+			Workflow:    c.CheckSuite.WorkflowRun.Workflow.Name,
 		}
+
 		switch state {
 		case "SUCCESS":
 			item.Bucket = "pass"
@@ -91,7 +96,7 @@ func eliminateDuplicates(checkContexts []api.CheckContext) []api.CheckContext {
 			}
 			mapContexts[ctx.Context] = struct{}{}
 		} else {
-			key := fmt.Sprintf("%s/%s", ctx.Name, ctx.CheckSuite.WorkflowRun.Workflow.Name)
+			key := fmt.Sprintf("%s/%s/%s", ctx.Name, ctx.CheckSuite.WorkflowRun.Workflow.Name, ctx.CheckSuite.WorkflowRun.Event)
 			if _, exists := mapChecks[key]; exists {
 				continue
 			}
diff --git a/pkg/cmd/pr/checks/output.go b/pkg/cmd/pr/checks/output.go
index bf5ecc839..524d3a4f5 100644
--- a/pkg/cmd/pr/checks/output.go
+++ b/pkg/cmd/pr/checks/output.go
@@ -34,8 +34,16 @@ func addRow(tp utils.TablePrinter, io *iostreams.IOStreams, o check) {
 	}
 
 	if io.IsStdoutTTY() {
+		var name string
+		if o.Workflow != "" {
+			name += fmt.Sprintf("%s/", o.Workflow)
+		}
+		name += o.Name
+		if o.Event != "" {
+			name += fmt.Sprintf(" (%s)", o.Event)
+		}
 		tp.AddField(mark, nil, markColor)
-		tp.AddField(o.Name, nil, nil)
+		tp.AddField(name, nil, nil)
 		tp.AddField(elapsed, nil, nil)
 		tp.AddField(o.Link, nil, nil)
 	} else {


Test patch:
diff --git a/pkg/cmd/auth/refresh/refresh_test.go b/pkg/cmd/auth/refresh/refresh_test.go
index 8743f1835..41d92144a 100644
--- a/pkg/cmd/auth/refresh/refresh_test.go
+++ b/pkg/cmd/auth/refresh/refresh_test.go
@@ -99,6 +99,46 @@ func Test_NewCmdRefresh(t *testing.T) {
 				InsecureStorage: true,
 			},
 		},
+		{
+			name: "reset scopes",
+			tty:  true,
+			cli:  "--reset-scopes",
+			wants: RefreshOptions{
+				ResetScopes: true,
+			},
+		},
+		{
+			name: "reset scopes shorthand",
+			tty:  true,
+			cli:  "-R",
+			wants: RefreshOptions{
+				ResetScopes: true,
+			},
+		},
+		{
+			name: "remove scope",
+			tty:  true,
+			cli:  "--remove-scope read:public_key",
+			wants: RefreshOptions{
+				RemoveScopes: []string{"read:public_key"},
+			},
+		},
+		{
+			name: "remove multiple scopes",
+			tty:  true,
+			cli:  "--remove-scope workflow,read:public_key",
+			wants: RefreshOptions{
+				RemoveScopes: []string{"workflow", "read:public_key"},
+			},
+		},
+		{
+			name: "remove scope shorthand",
+			tty:  true,
+			cli:  "-r read:public_key",
+			wants: RefreshOptions{
+				RemoveScopes: []string{"read:public_key"},
+			},
+		},
 	}
 
 	for _, tt := range tests {
@@ -280,6 +320,100 @@ func Test_refreshRun(t *testing.T) {
 				scopes:   nil,
 			},
 		},
+		{
+			name: "reset scopes",
+			cfgHosts: []string{
+				"github.com",
+			},
+			oldScopes: "delete_repo, codespace",
+			opts: &RefreshOptions{
+				Hostname:    "github.com",
+				ResetScopes: true,
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        nil,
+				secureStorage: true,
+			},
+		},
+		{
+			name: "reset scopes and add some scopes",
+			cfgHosts: []string{
+				"github.com",
+			},
+			oldScopes: "repo:invite, delete_repo, codespace",
+			opts: &RefreshOptions{
+				Scopes:      []string{"public_key:read", "workflow"},
+				ResetScopes: true,
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        []string{"public_key:read", "workflow"},
+				secureStorage: true,
+			},
+		},
+		{
+			name: "remove scopes",
+			cfgHosts: []string{
+				"github.com",
+			},
+			oldScopes: "delete_repo, codespace, repo:invite, public_key:read",
+			opts: &RefreshOptions{
+				Hostname:     "github.com",
+				RemoveScopes: []string{"delete_repo", "repo:invite"},
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        []string{"codespace", "public_key:read"},
+				secureStorage: true,
+			},
+		},
+		{
+			name: "remove scope but no old scope",
+			cfgHosts: []string{
+				"github.com",
+			},
+			opts: &RefreshOptions{
+				Hostname:     "github.com",
+				RemoveScopes: []string{"delete_repo"},
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        nil,
+				secureStorage: true,
+			},
+		},
+		{
+			name: "remove and add scopes at the same time",
+			cfgHosts: []string{
+				"github.com",
+			},
+			oldScopes: "repo:invite, delete_repo, codespace",
+			opts: &RefreshOptions{
+				Scopes:       []string{"repo:invite", "public_key:read", "workflow"},
+				RemoveScopes: []string{"codespace", "repo:invite", "workflow"},
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        []string{"public_key:read", "delete_repo"},
+				secureStorage: true,
+			},
+		},
+		{
+			name: "remove scopes that don't exist",
+			cfgHosts: []string{
+				"github.com",
+			},
+			oldScopes: "repo:invite, delete_repo, codespace",
+			opts: &RefreshOptions{
+				RemoveScopes: []string{"codespace", "repo:invite", "public_key:read"},
+			},
+			wantAuthArgs: authArgs{
+				hostname:      "github.com",
+				scopes:        []string{"delete_repo"},
+				secureStorage: true,
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/pr/checks/checks_test.go b/pkg/cmd/pr/checks/checks_test.go
index 186c1d324..ebdf52306 100644
--- a/pkg/cmd/pr/checks/checks_test.go
+++ b/pkg/cmd/pr/checks/checks_test.go
@@ -610,6 +610,156 @@ func TestEliminateDuplicates(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "unique workflow name",
+			checkContexts: []api.CheckContext{
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/1",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "some builds",
+							},
+						},
+					},
+				},
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/2",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "some other builds",
+							},
+						},
+					},
+				},
+			},
+			want: []api.CheckContext{
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/1",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "some builds",
+							},
+						},
+					},
+				},
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/2",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "some other builds",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "unique workflow run event",
+			checkContexts: []api.CheckContext{
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/1",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "builds",
+							},
+						},
+					},
+				},
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/2",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "pull_request",
+							Workflow: api.Workflow{
+								Name: "builds",
+							},
+						},
+					},
+				},
+			},
+			want: []api.CheckContext{
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/1",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "push",
+							Workflow: api.Workflow{
+								Name: "builds",
+							},
+						},
+					},
+				},
+				{
+					TypeName:    "CheckRun",
+					Name:        "build (ubuntu-latest)",
+					Status:      "COMPLETED",
+					Conclusion:  "SUCCESS",
+					StartedAt:   time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					CompletedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
+					DetailsURL:  "https://github.com/cli/cli/runs/2",
+					CheckSuite: api.CheckSuite{
+						WorkflowRun: api.WorkflowRun{
+							Event: "pull_request",
+							Workflow: api.Workflow{
+								Name: "builds",
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for _, tt := range tests {