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 <samcoe@users.noreply.github.com>
This commit is contained in:
Anton Baklanov 2022-01-26 03:14:56 -03:00 committed by GitHub
parent a564909f03
commit e28fa3c112
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 13 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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 <run-id>\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 {

View file

@ -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
}

View file

@ -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()

View file

@ -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)

View file

@ -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 {