From e28fa3c112edc7e6ac1590440607e69ca60538c8 Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Wed, 26 Jan 2022 03:14:56 -0300 Subject: [PATCH] Add branch and actor filters to `run list` (#4100) * Add branch and actor filters to `run list` * Simplify what FilterOptions can do * Check not only limit in TestNewCmdList * Verify that branch/actor params are parsed properly * Verify that API requests have proper query parameters * Change flag name from actor to user Co-authored-by: Sam Coe --- pkg/cmd/run/cancel/cancel.go | 2 +- pkg/cmd/run/list/list.go | 13 ++++++- pkg/cmd/run/list/list_test.go | 72 +++++++++++++++++++++++++++++++++++ pkg/cmd/run/rerun/rerun.go | 2 +- pkg/cmd/run/shared/shared.go | 27 +++++++++---- pkg/cmd/run/view/view.go | 2 +- pkg/cmd/run/watch/watch.go | 2 +- 7 files changed, 107 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/run/cancel/cancel.go b/pkg/cmd/run/cancel/cancel.go index b2d1d4543..39891986c 100644 --- a/pkg/cmd/run/cancel/cancel.go +++ b/pkg/cmd/run/cancel/cancel.go @@ -74,7 +74,7 @@ func runCancel(opts *CancelOptions) error { var run *shared.Run if opts.Prompt { - runs, err := shared.GetRunsWithFilter(client, repo, 10, func(run shared.Run) bool { + runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { return run.Status != shared.Completed }) if err != nil { diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index bf9c84623..d3465b114 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -29,6 +29,8 @@ type ListOptions struct { Limit int WorkflowSelector string + Branch string + Actor string } func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { @@ -62,6 +64,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd.Flags().IntVarP(&opts.Limit, "limit", "L", defaultLimit, "Maximum number of runs to fetch") cmd.Flags().StringVarP(&opts.WorkflowSelector, "workflow", "w", "", "Filter runs by workflow") + cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Filter runs by branch") + cmd.Flags().StringVarP(&opts.Actor, "user", "u", "", "Filter runs by user who triggered the run") cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.RunFields) return cmd @@ -82,16 +86,21 @@ func listRun(opts *ListOptions) error { var runs []shared.Run var workflow *workflowShared.Workflow + filters := &shared.FilterOptions{ + Branch: opts.Branch, + Actor: opts.Actor, + } + opts.IO.StartProgressIndicator() if opts.WorkflowSelector != "" { states := []workflowShared.WorkflowState{workflowShared.Active} workflow, err = workflowShared.ResolveWorkflow( opts.IO, client, baseRepo, false, opts.WorkflowSelector, states) if err == nil { - runs, err = shared.GetRunsByWorkflow(client, baseRepo, opts.Limit, workflow.ID) + runs, err = shared.GetRunsByWorkflow(client, baseRepo, filters, opts.Limit, workflow.ID) } } else { - runs, err = shared.GetRuns(client, baseRepo, opts.Limit) + runs, err = shared.GetRuns(client, baseRepo, filters, opts.Limit) } opts.IO.StopProgressIndicator() if err != nil { diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index 284fcbfba..fb18d04d2 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "testing" "github.com/cli/cli/v2/internal/ghrepo" @@ -51,6 +52,22 @@ func TestNewCmdList(t *testing.T) { WorkflowSelector: "foo.yml", }, }, + { + name: "branch", + cli: "--branch new-cool-stuff", + wants: ListOptions{ + Limit: defaultLimit, + Branch: "new-cool-stuff", + }, + }, + { + name: "user", + cli: "--user bak1an", + wants: ListOptions{ + Limit: defaultLimit, + Actor: "bak1an", + }, + }, } for _, tt := range tests { @@ -83,11 +100,32 @@ func TestNewCmdList(t *testing.T) { } assert.Equal(t, tt.wants.Limit, gotOpts.Limit) + assert.Equal(t, tt.wants.WorkflowSelector, gotOpts.WorkflowSelector) + assert.Equal(t, tt.wants.Branch, gotOpts.Branch) + assert.Equal(t, tt.wants.Actor, gotOpts.Actor) }) } } func TestListRun(t *testing.T) { + // helper to match mocked requests by their query params along with method and path + queryMatcher := func(method string, path string, query url.Values) httpmock.Matcher { + return func(req *http.Request) bool { + if !httpmock.REST(method, path)(req) { + return false + } + + actualQuery := req.URL.Query() + + for param := range query { + if !(actualQuery.Get(param) == query.Get(param)) { + return false + } + } + + return true + } + } tests := []struct { name string opts *ListOptions @@ -198,6 +236,40 @@ func TestListRun(t *testing.T) { }, wantOut: "STATUS NAME WORKFLOW BRANCH EVENT ID ELAPSED AGE\n* cool commit in progress trunk push 2 4m34s Feb 23, 2021\n✓ cool commit successful trunk push 3 4m34s Feb 23, 2021\nX cool commit failed trunk push 1234 4m34s Feb 23, 2021\n\nFor details on a run, try: gh run view \n", }, + { + name: "branch filter applied", + opts: &ListOptions{ + Limit: defaultLimit, + Branch: "the-branch", + }, + stubs: func(reg *httpmock.Registry) { + reg.Register( + queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ + "branch": []string{"the-branch"}, + }), + httpmock.JSONResponse(shared.RunsPayload{}), + ) + }, + wantOut: "", + wantErrOut: "No runs found\n", + }, + { + name: "actor filter applied", + opts: &ListOptions{ + Limit: defaultLimit, + Actor: "bak1an", + }, + stubs: func(reg *httpmock.Registry) { + reg.Register( + queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ + "actor": []string{"bak1an"}, + }), + httpmock.JSONResponse(shared.RunsPayload{}), + ) + }, + wantOut: "", + wantErrOut: "No runs found\n", + }, } for _, tt := range tests { diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index feec21599..9788a3061 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -71,7 +71,7 @@ func runRerun(opts *RerunOptions) error { if opts.Prompt { cs := opts.IO.ColorScheme() - runs, err := shared.GetRunsWithFilter(client, repo, 10, func(run shared.Run) bool { + runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { if run.Status != shared.Completed { return false } diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 4e104e2ce..b015ecc0e 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -205,9 +205,14 @@ type RunsPayload struct { WorkflowRuns []Run `json:"workflow_runs"` } -func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, limit int, f func(Run) bool) ([]Run, error) { +type FilterOptions struct { + Branch string + Actor string +} + +func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int, f func(Run) bool) ([]Run, error) { path := fmt.Sprintf("repos/%s/actions/runs", ghrepo.FullName(repo)) - runs, err := getRuns(client, repo, path, 50) + runs, err := getRuns(client, repo, path, opts, 50) if err != nil { return nil, err } @@ -224,17 +229,17 @@ func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, limit int, f f return filtered, nil } -func GetRunsByWorkflow(client *api.Client, repo ghrepo.Interface, limit int, workflowID int64) ([]Run, error) { +func GetRunsByWorkflow(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int, workflowID int64) ([]Run, error) { path := fmt.Sprintf("repos/%s/actions/workflows/%d/runs", ghrepo.FullName(repo), workflowID) - return getRuns(client, repo, path, limit) + return getRuns(client, repo, path, opts, limit) } -func GetRuns(client *api.Client, repo ghrepo.Interface, limit int) ([]Run, error) { +func GetRuns(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int) ([]Run, error) { path := fmt.Sprintf("repos/%s/actions/runs", ghrepo.FullName(repo)) - return getRuns(client, repo, path, limit) + return getRuns(client, repo, path, opts, limit) } -func getRuns(client *api.Client, repo ghrepo.Interface, path string, limit int) ([]Run, error) { +func getRuns(client *api.Client, repo ghrepo.Interface, path string, opts *FilterOptions, limit int) ([]Run, error) { perPage := limit page := 1 if limit > 100 { @@ -253,6 +258,14 @@ func getRuns(client *api.Client, repo ghrepo.Interface, path string, limit int) query := parsed.Query() query.Set("per_page", fmt.Sprintf("%d", perPage)) query.Set("page", fmt.Sprintf("%d", page)) + if opts != nil { + if opts.Branch != "" { + query.Set("branch", opts.Branch) + } + if opts.Actor != "" { + query.Set("actor", opts.Actor) + } + } parsed.RawQuery = query.Encode() pagedPath := parsed.String() diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index dd8b30165..73dea9e6a 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -197,7 +197,7 @@ func runView(opts *ViewOptions) error { if opts.Prompt { // TODO arbitrary limit opts.IO.StartProgressIndicator() - runs, err := shared.GetRuns(client, repo, 10) + runs, err := shared.GetRuns(client, repo, nil, 10) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get runs: %w", err) diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index 53b1437bc..5e397c4f2 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -93,7 +93,7 @@ func watchRun(opts *WatchOptions) error { var run *shared.Run if opts.Prompt { - runs, err := shared.GetRunsWithFilter(client, repo, 10, func(run shared.Run) bool { + runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { return run.Status != shared.Completed }) if err != nil {