diff --git a/pkg/cmd/run/cancel/cancel_test.go b/pkg/cmd/run/cancel/cancel_test.go index 4875918d7..bcde453c2 100644 --- a/pkg/cmd/run/cancel/cancel_test.go +++ b/pkg/cmd/run/cancel/cancel_test.go @@ -8,6 +8,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/run/shared" + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -82,8 +83,8 @@ func TestNewCmdCancel(t *testing.T) { } func TestRunCancel(t *testing.T) { - inProgressRun := shared.TestRun("more runs", 1234, shared.InProgress, "") - completedRun := shared.TestRun("more runs", 4567, shared.Completed, shared.Failure) + inProgressRun := shared.TestRun(1234, shared.InProgress, "") + completedRun := shared.TestRun(4567, shared.Completed, shared.Failure) tests := []struct { name string httpStubs func(*httpmock.Registry) @@ -103,6 +104,9 @@ func TestRunCancel(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(inProgressRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"), httpmock.StatusStringResponse(202, "{}")) @@ -133,6 +137,9 @@ func TestRunCancel(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/4567"), httpmock.JSONResponse(completedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/4567/cancel"), httpmock.StatusStringResponse(409, ""), @@ -154,6 +161,13 @@ func TestRunCancel(t *testing.T) { completedRun, }, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, }, { @@ -169,6 +183,13 @@ func TestRunCancel(t *testing.T) { inProgressRun, }, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/cancel"), httpmock.StatusStringResponse(202, "{}")) diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index c1a06d0a1..2bd57ca92 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -84,9 +84,6 @@ func listRun(opts *ListOptions) error { } client := api.NewClientFromHTTP(c) - var runs []shared.Run - var workflow *workflowShared.Workflow - filters := &shared.FilterOptions{ Branch: opts.Branch, Actor: opts.Actor, @@ -95,18 +92,19 @@ func listRun(opts *ListOptions) error { 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, filters, opts.Limit, workflow.ID) + if workflow, err := workflowShared.ResolveWorkflow(opts.IO, client, baseRepo, false, opts.WorkflowSelector, states); err == nil { + filters.WorkflowID = workflow.ID + filters.WorkflowName = workflow.Name + } else { + return err } - } else { - runs, err = shared.GetRuns(client, baseRepo, filters, opts.Limit) } + runsResult, err := shared.GetRuns(client, baseRepo, filters, opts.Limit) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get runs: %w", err) } + runs := runsResult.WorkflowRuns if err := opts.IO.StartPager(); err == nil { defer opts.IO.StopPager() @@ -128,7 +126,7 @@ func listRun(opts *ListOptions) error { if tp.IsTTY() { tp.AddField("STATUS", nil, nil) - tp.AddField("NAME", nil, nil) + tp.AddField("TITLE", nil, nil) tp.AddField("WORKFLOW", nil, nil) tp.AddField("BRANCH", nil, nil) tp.AddField("EVENT", nil, nil) @@ -147,9 +145,9 @@ func listRun(opts *ListOptions) error { tp.AddField(string(run.Conclusion), nil, nil) } - tp.AddField(run.CommitMsg(), nil, cs.Bold) + tp.AddField(run.Title(), nil, cs.Bold) - tp.AddField(run.Name, nil, nil) + tp.AddField(run.WorkflowName(), nil, nil) tp.AddField(run.HeadBranch, nil, cs.Bold) tp.AddField(string(run.Event), nil, nil) tp.AddField(fmt.Sprintf("%d", run.ID), nil, cs.Cyan) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index 646f1a509..aa7fe993c 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -2,13 +2,13 @@ package list import ( "bytes" - "fmt" "io" "net/http" "net/url" "testing" "time" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/run/shared" workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" @@ -131,8 +131,27 @@ func TestListRun(t *testing.T) { httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: shared.TestRuns, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, - wantOut: "STATUS NAME WORKFLOW BRANCH EVENT ID ELAPSED AGE\nX cool commit timed out trunk push 1 4m34s Feb 23, 2021\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 cancelled trunk push 4 4m34s Feb 23, 2021\nX cool commit failed trunk push 1234 4m34s Feb 23, 2021\n- cool commit neutral trunk push 6 4m34s Feb 23, 2021\n- cool commit skipped trunk push 7 4m34s Feb 23, 2021\n* cool commit requested trunk push 8 4m34s Feb 23, 2021\n* cool commit queued trunk push 9 4m34s Feb 23, 2021\nX cool commit stale trunk push 10 4m34s Feb 23, 2021\n", + wantOut: heredoc.Doc(` + STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE + X cool commit CI trunk push 1 4m34s Feb 23, 2021 + * cool commit CI trunk push 2 4m34s Feb 23, 2021 + ✓ cool commit CI trunk push 3 4m34s Feb 23, 2021 + X cool commit CI trunk push 4 4m34s Feb 23, 2021 + X cool commit CI trunk push 1234 4m34s Feb 23, 2021 + - cool commit CI trunk push 6 4m34s Feb 23, 2021 + - cool commit CI trunk push 7 4m34s Feb 23, 2021 + * cool commit CI trunk push 8 4m34s Feb 23, 2021 + * cool commit CI trunk push 9 4m34s Feb 23, 2021 + X cool commit CI trunk push 10 4m34s Feb 23, 2021 + `), }, { name: "default arguments nontty", @@ -147,8 +166,26 @@ func TestListRun(t *testing.T) { httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: shared.TestRuns, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, - wantOut: "completed\ttimed_out\tcool commit\ttimed out\ttrunk\tpush\t1\t4m34s\tFeb 23, 2021\nin_progress\t\tcool commit\tin progress\ttrunk\tpush\t2\t4m34s\tFeb 23, 2021\ncompleted\tsuccess\tcool commit\tsuccessful\ttrunk\tpush\t3\t4m34s\tFeb 23, 2021\ncompleted\tcancelled\tcool commit\tcancelled\ttrunk\tpush\t4\t4m34s\tFeb 23, 2021\ncompleted\tfailure\tcool commit\tfailed\ttrunk\tpush\t1234\t4m34s\tFeb 23, 2021\ncompleted\tneutral\tcool commit\tneutral\ttrunk\tpush\t6\t4m34s\tFeb 23, 2021\ncompleted\tskipped\tcool commit\tskipped\ttrunk\tpush\t7\t4m34s\tFeb 23, 2021\nrequested\t\tcool commit\trequested\ttrunk\tpush\t8\t4m34s\tFeb 23, 2021\nqueued\t\tcool commit\tqueued\ttrunk\tpush\t9\t4m34s\tFeb 23, 2021\ncompleted\tstale\tcool commit\tstale\ttrunk\tpush\t10\t4m34s\tFeb 23, 2021\n", + wantOut: heredoc.Doc(` + completed timed_out cool commit CI trunk push 1 4m34s Feb 23, 2021 + in_progress cool commit CI trunk push 2 4m34s Feb 23, 2021 + completed success cool commit CI trunk push 3 4m34s Feb 23, 2021 + completed cancelled cool commit CI trunk push 4 4m34s Feb 23, 2021 + completed failure cool commit CI trunk push 1234 4m34s Feb 23, 2021 + completed neutral cool commit CI trunk push 6 4m34s Feb 23, 2021 + completed skipped cool commit CI trunk push 7 4m34s Feb 23, 2021 + requested cool commit CI trunk push 8 4m34s Feb 23, 2021 + queued cool commit CI trunk push 9 4m34s Feb 23, 2021 + completed stale cool commit CI trunk push 10 4m34s Feb 23, 2021 + `), }, { name: "pagination", @@ -161,21 +198,131 @@ func TestListRun(t *testing.T) { var runID int64 runs := []shared.Run{} for runID < 103 { - runs = append(runs, shared.TestRun(fmt.Sprintf("%d", runID), runID, shared.InProgress, "")) + runs = append(runs, shared.TestRun(runID, shared.InProgress, "")) runID++ } reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), - httpmock.JSONResponse(shared.RunsPayload{ + httpmock.WithHeader(httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: runs[0:100], - })) + }), "Link", `; rel="next"`)) reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.REST("GET", "repositories/123/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: runs[100:], })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, - wantOut: longRunOutput, + wantOut: heredoc.Doc(` + STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE + * cool commit CI trunk push 0 4m34s Feb 23, 2021 + * cool commit CI trunk push 1 4m34s Feb 23, 2021 + * cool commit CI trunk push 2 4m34s Feb 23, 2021 + * cool commit CI trunk push 3 4m34s Feb 23, 2021 + * cool commit CI trunk push 4 4m34s Feb 23, 2021 + * cool commit CI trunk push 5 4m34s Feb 23, 2021 + * cool commit CI trunk push 6 4m34s Feb 23, 2021 + * cool commit CI trunk push 7 4m34s Feb 23, 2021 + * cool commit CI trunk push 8 4m34s Feb 23, 2021 + * cool commit CI trunk push 9 4m34s Feb 23, 2021 + * cool commit CI trunk push 10 4m34s Feb 23, 2021 + * cool commit CI trunk push 11 4m34s Feb 23, 2021 + * cool commit CI trunk push 12 4m34s Feb 23, 2021 + * cool commit CI trunk push 13 4m34s Feb 23, 2021 + * cool commit CI trunk push 14 4m34s Feb 23, 2021 + * cool commit CI trunk push 15 4m34s Feb 23, 2021 + * cool commit CI trunk push 16 4m34s Feb 23, 2021 + * cool commit CI trunk push 17 4m34s Feb 23, 2021 + * cool commit CI trunk push 18 4m34s Feb 23, 2021 + * cool commit CI trunk push 19 4m34s Feb 23, 2021 + * cool commit CI trunk push 20 4m34s Feb 23, 2021 + * cool commit CI trunk push 21 4m34s Feb 23, 2021 + * cool commit CI trunk push 22 4m34s Feb 23, 2021 + * cool commit CI trunk push 23 4m34s Feb 23, 2021 + * cool commit CI trunk push 24 4m34s Feb 23, 2021 + * cool commit CI trunk push 25 4m34s Feb 23, 2021 + * cool commit CI trunk push 26 4m34s Feb 23, 2021 + * cool commit CI trunk push 27 4m34s Feb 23, 2021 + * cool commit CI trunk push 28 4m34s Feb 23, 2021 + * cool commit CI trunk push 29 4m34s Feb 23, 2021 + * cool commit CI trunk push 30 4m34s Feb 23, 2021 + * cool commit CI trunk push 31 4m34s Feb 23, 2021 + * cool commit CI trunk push 32 4m34s Feb 23, 2021 + * cool commit CI trunk push 33 4m34s Feb 23, 2021 + * cool commit CI trunk push 34 4m34s Feb 23, 2021 + * cool commit CI trunk push 35 4m34s Feb 23, 2021 + * cool commit CI trunk push 36 4m34s Feb 23, 2021 + * cool commit CI trunk push 37 4m34s Feb 23, 2021 + * cool commit CI trunk push 38 4m34s Feb 23, 2021 + * cool commit CI trunk push 39 4m34s Feb 23, 2021 + * cool commit CI trunk push 40 4m34s Feb 23, 2021 + * cool commit CI trunk push 41 4m34s Feb 23, 2021 + * cool commit CI trunk push 42 4m34s Feb 23, 2021 + * cool commit CI trunk push 43 4m34s Feb 23, 2021 + * cool commit CI trunk push 44 4m34s Feb 23, 2021 + * cool commit CI trunk push 45 4m34s Feb 23, 2021 + * cool commit CI trunk push 46 4m34s Feb 23, 2021 + * cool commit CI trunk push 47 4m34s Feb 23, 2021 + * cool commit CI trunk push 48 4m34s Feb 23, 2021 + * cool commit CI trunk push 49 4m34s Feb 23, 2021 + * cool commit CI trunk push 50 4m34s Feb 23, 2021 + * cool commit CI trunk push 51 4m34s Feb 23, 2021 + * cool commit CI trunk push 52 4m34s Feb 23, 2021 + * cool commit CI trunk push 53 4m34s Feb 23, 2021 + * cool commit CI trunk push 54 4m34s Feb 23, 2021 + * cool commit CI trunk push 55 4m34s Feb 23, 2021 + * cool commit CI trunk push 56 4m34s Feb 23, 2021 + * cool commit CI trunk push 57 4m34s Feb 23, 2021 + * cool commit CI trunk push 58 4m34s Feb 23, 2021 + * cool commit CI trunk push 59 4m34s Feb 23, 2021 + * cool commit CI trunk push 60 4m34s Feb 23, 2021 + * cool commit CI trunk push 61 4m34s Feb 23, 2021 + * cool commit CI trunk push 62 4m34s Feb 23, 2021 + * cool commit CI trunk push 63 4m34s Feb 23, 2021 + * cool commit CI trunk push 64 4m34s Feb 23, 2021 + * cool commit CI trunk push 65 4m34s Feb 23, 2021 + * cool commit CI trunk push 66 4m34s Feb 23, 2021 + * cool commit CI trunk push 67 4m34s Feb 23, 2021 + * cool commit CI trunk push 68 4m34s Feb 23, 2021 + * cool commit CI trunk push 69 4m34s Feb 23, 2021 + * cool commit CI trunk push 70 4m34s Feb 23, 2021 + * cool commit CI trunk push 71 4m34s Feb 23, 2021 + * cool commit CI trunk push 72 4m34s Feb 23, 2021 + * cool commit CI trunk push 73 4m34s Feb 23, 2021 + * cool commit CI trunk push 74 4m34s Feb 23, 2021 + * cool commit CI trunk push 75 4m34s Feb 23, 2021 + * cool commit CI trunk push 76 4m34s Feb 23, 2021 + * cool commit CI trunk push 77 4m34s Feb 23, 2021 + * cool commit CI trunk push 78 4m34s Feb 23, 2021 + * cool commit CI trunk push 79 4m34s Feb 23, 2021 + * cool commit CI trunk push 80 4m34s Feb 23, 2021 + * cool commit CI trunk push 81 4m34s Feb 23, 2021 + * cool commit CI trunk push 82 4m34s Feb 23, 2021 + * cool commit CI trunk push 83 4m34s Feb 23, 2021 + * cool commit CI trunk push 84 4m34s Feb 23, 2021 + * cool commit CI trunk push 85 4m34s Feb 23, 2021 + * cool commit CI trunk push 86 4m34s Feb 23, 2021 + * cool commit CI trunk push 87 4m34s Feb 23, 2021 + * cool commit CI trunk push 88 4m34s Feb 23, 2021 + * cool commit CI trunk push 89 4m34s Feb 23, 2021 + * cool commit CI trunk push 90 4m34s Feb 23, 2021 + * cool commit CI trunk push 91 4m34s Feb 23, 2021 + * cool commit CI trunk push 92 4m34s Feb 23, 2021 + * cool commit CI trunk push 93 4m34s Feb 23, 2021 + * cool commit CI trunk push 94 4m34s Feb 23, 2021 + * cool commit CI trunk push 95 4m34s Feb 23, 2021 + * cool commit CI trunk push 96 4m34s Feb 23, 2021 + * cool commit CI trunk push 97 4m34s Feb 23, 2021 + * cool commit CI trunk push 98 4m34s Feb 23, 2021 + * cool commit CI trunk push 99 4m34s Feb 23, 2021 + * cool commit CI trunk push 100 4m34s Feb 23, 2021 + `), }, { name: "no results", @@ -208,7 +355,12 @@ func TestListRun(t *testing.T) { WorkflowRuns: shared.WorkflowRuns, })) }, - 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", + wantOut: heredoc.Doc(` + STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE + * cool commit a workflow trunk push 2 4m34s Feb 23, 2021 + ✓ cool commit a workflow trunk push 3 4m34s Feb 23, 2021 + X cool commit a workflow trunk push 1234 4m34s Feb 23, 2021 + `), }, { name: "branch filter applied", @@ -273,5 +425,3 @@ func TestListRun(t *testing.T) { }) } } - -const longRunOutput = "STATUS NAME WORKFLOW BRANCH EVENT ID ELAPSED AGE\n* cool commit 0 trunk push 0 4m34s Feb 23, 2021\n* cool commit 1 trunk push 1 4m34s Feb 23, 2021\n* cool commit 2 trunk push 2 4m34s Feb 23, 2021\n* cool commit 3 trunk push 3 4m34s Feb 23, 2021\n* cool commit 4 trunk push 4 4m34s Feb 23, 2021\n* cool commit 5 trunk push 5 4m34s Feb 23, 2021\n* cool commit 6 trunk push 6 4m34s Feb 23, 2021\n* cool commit 7 trunk push 7 4m34s Feb 23, 2021\n* cool commit 8 trunk push 8 4m34s Feb 23, 2021\n* cool commit 9 trunk push 9 4m34s Feb 23, 2021\n* cool commit 10 trunk push 10 4m34s Feb 23, 2021\n* cool commit 11 trunk push 11 4m34s Feb 23, 2021\n* cool commit 12 trunk push 12 4m34s Feb 23, 2021\n* cool commit 13 trunk push 13 4m34s Feb 23, 2021\n* cool commit 14 trunk push 14 4m34s Feb 23, 2021\n* cool commit 15 trunk push 15 4m34s Feb 23, 2021\n* cool commit 16 trunk push 16 4m34s Feb 23, 2021\n* cool commit 17 trunk push 17 4m34s Feb 23, 2021\n* cool commit 18 trunk push 18 4m34s Feb 23, 2021\n* cool commit 19 trunk push 19 4m34s Feb 23, 2021\n* cool commit 20 trunk push 20 4m34s Feb 23, 2021\n* cool commit 21 trunk push 21 4m34s Feb 23, 2021\n* cool commit 22 trunk push 22 4m34s Feb 23, 2021\n* cool commit 23 trunk push 23 4m34s Feb 23, 2021\n* cool commit 24 trunk push 24 4m34s Feb 23, 2021\n* cool commit 25 trunk push 25 4m34s Feb 23, 2021\n* cool commit 26 trunk push 26 4m34s Feb 23, 2021\n* cool commit 27 trunk push 27 4m34s Feb 23, 2021\n* cool commit 28 trunk push 28 4m34s Feb 23, 2021\n* cool commit 29 trunk push 29 4m34s Feb 23, 2021\n* cool commit 30 trunk push 30 4m34s Feb 23, 2021\n* cool commit 31 trunk push 31 4m34s Feb 23, 2021\n* cool commit 32 trunk push 32 4m34s Feb 23, 2021\n* cool commit 33 trunk push 33 4m34s Feb 23, 2021\n* cool commit 34 trunk push 34 4m34s Feb 23, 2021\n* cool commit 35 trunk push 35 4m34s Feb 23, 2021\n* cool commit 36 trunk push 36 4m34s Feb 23, 2021\n* cool commit 37 trunk push 37 4m34s Feb 23, 2021\n* cool commit 38 trunk push 38 4m34s Feb 23, 2021\n* cool commit 39 trunk push 39 4m34s Feb 23, 2021\n* cool commit 40 trunk push 40 4m34s Feb 23, 2021\n* cool commit 41 trunk push 41 4m34s Feb 23, 2021\n* cool commit 42 trunk push 42 4m34s Feb 23, 2021\n* cool commit 43 trunk push 43 4m34s Feb 23, 2021\n* cool commit 44 trunk push 44 4m34s Feb 23, 2021\n* cool commit 45 trunk push 45 4m34s Feb 23, 2021\n* cool commit 46 trunk push 46 4m34s Feb 23, 2021\n* cool commit 47 trunk push 47 4m34s Feb 23, 2021\n* cool commit 48 trunk push 48 4m34s Feb 23, 2021\n* cool commit 49 trunk push 49 4m34s Feb 23, 2021\n* cool commit 50 trunk push 50 4m34s Feb 23, 2021\n* cool commit 51 trunk push 51 4m34s Feb 23, 2021\n* cool commit 52 trunk push 52 4m34s Feb 23, 2021\n* cool commit 53 trunk push 53 4m34s Feb 23, 2021\n* cool commit 54 trunk push 54 4m34s Feb 23, 2021\n* cool commit 55 trunk push 55 4m34s Feb 23, 2021\n* cool commit 56 trunk push 56 4m34s Feb 23, 2021\n* cool commit 57 trunk push 57 4m34s Feb 23, 2021\n* cool commit 58 trunk push 58 4m34s Feb 23, 2021\n* cool commit 59 trunk push 59 4m34s Feb 23, 2021\n* cool commit 60 trunk push 60 4m34s Feb 23, 2021\n* cool commit 61 trunk push 61 4m34s Feb 23, 2021\n* cool commit 62 trunk push 62 4m34s Feb 23, 2021\n* cool commit 63 trunk push 63 4m34s Feb 23, 2021\n* cool commit 64 trunk push 64 4m34s Feb 23, 2021\n* cool commit 65 trunk push 65 4m34s Feb 23, 2021\n* cool commit 66 trunk push 66 4m34s Feb 23, 2021\n* cool commit 67 trunk push 67 4m34s Feb 23, 2021\n* cool commit 68 trunk push 68 4m34s Feb 23, 2021\n* cool commit 69 trunk push 69 4m34s Feb 23, 2021\n* cool commit 70 trunk push 70 4m34s Feb 23, 2021\n* cool commit 71 trunk push 71 4m34s Feb 23, 2021\n* cool commit 72 trunk push 72 4m34s Feb 23, 2021\n* cool commit 73 trunk push 73 4m34s Feb 23, 2021\n* cool commit 74 trunk push 74 4m34s Feb 23, 2021\n* cool commit 75 trunk push 75 4m34s Feb 23, 2021\n* cool commit 76 trunk push 76 4m34s Feb 23, 2021\n* cool commit 77 trunk push 77 4m34s Feb 23, 2021\n* cool commit 78 trunk push 78 4m34s Feb 23, 2021\n* cool commit 79 trunk push 79 4m34s Feb 23, 2021\n* cool commit 80 trunk push 80 4m34s Feb 23, 2021\n* cool commit 81 trunk push 81 4m34s Feb 23, 2021\n* cool commit 82 trunk push 82 4m34s Feb 23, 2021\n* cool commit 83 trunk push 83 4m34s Feb 23, 2021\n* cool commit 84 trunk push 84 4m34s Feb 23, 2021\n* cool commit 85 trunk push 85 4m34s Feb 23, 2021\n* cool commit 86 trunk push 86 4m34s Feb 23, 2021\n* cool commit 87 trunk push 87 4m34s Feb 23, 2021\n* cool commit 88 trunk push 88 4m34s Feb 23, 2021\n* cool commit 89 trunk push 89 4m34s Feb 23, 2021\n* cool commit 90 trunk push 90 4m34s Feb 23, 2021\n* cool commit 91 trunk push 91 4m34s Feb 23, 2021\n* cool commit 92 trunk push 92 4m34s Feb 23, 2021\n* cool commit 93 trunk push 93 4m34s Feb 23, 2021\n* cool commit 94 trunk push 94 4m34s Feb 23, 2021\n* cool commit 95 trunk push 95 4m34s Feb 23, 2021\n* cool commit 96 trunk push 96 4m34s Feb 23, 2021\n* cool commit 97 trunk push 97 4m34s Feb 23, 2021\n* cool commit 98 trunk push 98 4m34s Feb 23, 2021\n* cool commit 99 trunk push 99 4m34s Feb 23, 2021\n* cool commit 100 trunk push 100 4m34s Feb 23, 2021\n" diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 5d9dd3dc0..730952f1f 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/run/shared" + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -180,6 +181,13 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) @@ -197,6 +205,13 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), httpmock.StringResponse("{}")) @@ -230,6 +245,13 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) @@ -249,6 +271,13 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), httpmock.StringResponse("{}")) @@ -289,6 +318,20 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) @@ -311,8 +354,15 @@ func TestRerun(t *testing.T) { httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: []shared.Run{ shared.SuccessfulRun, - shared.TestRun("in progress", 2, shared.InProgress, ""), + shared.TestRun(2, shared.InProgress, ""), }})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, wantErr: true, errOut: "no recent runs have failed; please specify a specific ``", @@ -327,6 +377,13 @@ func TestRerun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/3/rerun"), httpmock.StatusStringResponse(403, "no")) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index 90f0fe6d0..900bd07a1 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -9,7 +9,7 @@ import ( func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string) string { title := fmt.Sprintf("%s %s%s", - cs.Bold(run.HeadBranch), run.Name, prNumber) + cs.Bold(run.HeadBranch), run.WorkflowName(), prNumber) symbol, symbolColor := Symbol(cs, run.Status, run.Conclusion) id := cs.Cyanf("%d", run.ID) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 6eb2f3300..e73b2d7b5 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -12,6 +12,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/prompt" ) @@ -45,6 +46,7 @@ type Level string var RunFields = []string{ "name", + "displayTitle", "headBranch", "headSha", "createdAt", @@ -55,11 +57,13 @@ var RunFields = []string{ "event", "databaseId", "workflowDatabaseId", + "workflowName", "url", } type Run struct { - Name string + Name string `json:"name"` // the semantics of this field are unclear + DisplayTitle string `json:"display_title"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` StartedAt time.Time `json:"run_started_at"` @@ -67,6 +71,7 @@ type Run struct { Conclusion Conclusion Event string ID int64 + workflowName string // cache column WorkflowID int64 `json:"workflow_id"` Attempts uint8 `json:"run_attempt"` HeadBranch string `json:"head_branch"` @@ -107,7 +112,12 @@ type Commit struct { Message string } -func (r Run) CommitMsg() string { +// Title is the display title for a run, falling back to the commit subject if unavailable +func (r Run) Title() string { + if r.DisplayTitle != "" { + return r.DisplayTitle + } + commitLines := strings.Split(r.HeadCommit.Message, "\n") if len(commitLines) > 0 { return commitLines[0] @@ -116,6 +126,12 @@ func (r Run) CommitMsg() string { } } +// WorkflowName returns the human-readable name of the workflow that this run belongs to. +// TODO: consider lazy-loading the underlying API data to avoid extra API calls unless necessary +func (r Run) WorkflowName() string { + return r.workflowName +} + func (r *Run) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(r).Elem() fieldByName := func(v reflect.Value, field string) reflect.Value { @@ -131,6 +147,8 @@ func (r *Run) ExportData(fields []string) map[string]interface{} { data[f] = r.ID case "workflowDatabaseId": data[f] = r.WorkflowID + case "workflowName": + data[f] = r.WorkflowName() default: sf := fieldByName(v, f) data[f] = sf.Interface() @@ -228,18 +246,22 @@ type RunsPayload struct { } type FilterOptions struct { - Branch string - Actor string + Branch string + Actor string + WorkflowID int64 + // avoid loading workflow name separately and use the provided one + WorkflowName string } +// GetRunsWithFilter fetches 50 runs from the API and filters them in-memory 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, opts, 50) + runs, err := GetRuns(client, repo, opts, 50) if err != nil { return nil, err } - filtered := []Run{} - for _, run := range runs { + + var filtered []Run + for _, run := range runs.WorkflowRuns { if f(run) { filtered = append(filtered, run) } @@ -251,70 +273,89 @@ func GetRunsWithFilter(client *api.Client, repo ghrepo.Interface, opts *FilterOp return filtered, nil } -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, opts, limit) -} - -func GetRuns(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int) ([]Run, error) { +func GetRuns(client *api.Client, repo ghrepo.Interface, opts *FilterOptions, limit int) (*RunsPayload, error) { path := fmt.Sprintf("repos/%s/actions/runs", ghrepo.FullName(repo)) - return getRuns(client, repo, path, opts, limit) -} + if opts != nil && opts.WorkflowID > 0 { + path = fmt.Sprintf("repos/%s/actions/workflows/%d/runs", ghrepo.FullName(repo), opts.WorkflowID) + } -func getRuns(client *api.Client, repo ghrepo.Interface, path string, opts *FilterOptions, limit int) ([]Run, error) { perPage := limit - page := 1 if limit > 100 { perPage = 100 } + path += fmt.Sprintf("?per_page=%d", perPage) - runs := []Run{} - - for len(runs) < limit { - var result RunsPayload - - parsed, err := url.Parse(path) - if err != nil { - return nil, err + if opts != nil { + if opts.Branch != "" { + path += fmt.Sprintf("&branch=%s", url.QueryEscape(opts.Branch)) } - 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) - } + if opts.Actor != "" { + path += fmt.Sprintf("&actor=%s", url.QueryEscape(opts.Actor)) } - parsed.RawQuery = query.Encode() - pagedPath := parsed.String() - - err = client.REST(repo.RepoHost(), "GET", pagedPath, nil, &result) - if err != nil { - return nil, err - } - - if len(result.WorkflowRuns) == 0 { - break - } - - for _, run := range result.WorkflowRuns { - runs = append(runs, run) - if len(runs) == limit { - break - } - } - - if len(result.WorkflowRuns) < perPage { - break - } - - page++ } - return runs, nil + var result *RunsPayload + +pagination: + for path != "" { + var response RunsPayload + var err error + path, err = client.RESTWithNext(repo.RepoHost(), "GET", path, nil, &response) + if err != nil { + return nil, err + } + + if result == nil { + result = &response + if len(result.WorkflowRuns) == limit { + break pagination + } + } else { + for _, run := range response.WorkflowRuns { + result.WorkflowRuns = append(result.WorkflowRuns, run) + if len(result.WorkflowRuns) == limit { + break pagination + } + } + } + } + + if opts != nil && opts.WorkflowName != "" { + for i := range result.WorkflowRuns { + result.WorkflowRuns[i].workflowName = opts.WorkflowName + } + } else if len(result.WorkflowRuns) > 0 { + if err := preloadWorkflowNames(client, repo, result.WorkflowRuns); err != nil { + return result, err + } + } + + return result, nil +} + +func preloadWorkflowNames(client *api.Client, repo ghrepo.Interface, runs []Run) error { + workflows, err := workflowShared.GetWorkflows(client, repo, 0) + if err != nil { + return err + } + + workflowMap := map[int64]string{} + for _, wf := range workflows { + workflowMap[wf.ID] = wf.Name + } + + for i, run := range runs { + if _, ok := workflowMap[run.WorkflowID]; !ok { + // Look up workflow by ID because it may have been deleted + workflow, err := workflowShared.GetWorkflow(client, repo, run.WorkflowID) + if err != nil { + return err + } + workflowMap[run.WorkflowID] = workflow.Name + } + runs[i].workflowName = workflowMap[run.WorkflowID] + } + return nil } type JobsPayload struct { @@ -351,7 +392,7 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { symbol, _ := Symbol(cs, run.Status, run.Conclusion) candidates = append(candidates, // TODO truncate commit message, long ones look terrible - fmt.Sprintf("%s %s, %s (%s) %s", symbol, run.CommitMsg(), run.Name, run.HeadBranch, preciseAgo(now, run.StartedTime()))) + fmt.Sprintf("%s %s, %s (%s) %s", symbol, run.Title(), run.WorkflowName(), run.HeadBranch, preciseAgo(now, run.StartedTime()))) } // TODO consider custom filter so it's fuzzier. right now matches start anywhere in string but @@ -380,6 +421,14 @@ func GetRun(client *api.Client, repo ghrepo.Interface, runID string) (*Run, erro return nil, err } + // Set name to workflow name + workflow, err := workflowShared.GetWorkflow(client, repo, result.WorkflowID) + if err != nil { + return nil, err + } else { + result.workflowName = workflow.Name + } + return &result, nil } diff --git a/pkg/cmd/run/shared/test.go b/pkg/cmd/run/shared/test.go index b413e4386..0e37572e3 100644 --- a/pkg/cmd/run/shared/test.go +++ b/pkg/cmd/run/shared/test.go @@ -3,13 +3,19 @@ package shared import ( "fmt" "time" + + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" ) var TestRunStartTime, _ = time.Parse("2006-01-02 15:04:05", "2021-02-23 04:51:00") -func TestRun(name string, id int64, s Status, c Conclusion) Run { +func TestRun(id int64, s Status, c Conclusion) Run { + return TestRunWithCommit(id, s, c, "cool commit") +} + +func TestRunWithCommit(id int64, s Status, c Conclusion, commit string) Run { return Run{ - Name: name, + WorkflowID: 123, ID: id, CreatedAt: TestRunStartTime, UpdatedAt: TestRunStartTime.Add(time.Minute*4 + time.Second*34), @@ -19,7 +25,7 @@ func TestRun(name string, id int64, s Status, c Conclusion) Run { HeadBranch: "trunk", JobsURL: fmt.Sprintf("https://api.github.com/runs/%d/jobs", id), HeadCommit: Commit{ - Message: "cool commit", + Message: commit, }, HeadSha: "1234567890", URL: fmt.Sprintf("https://github.com/runs/%d", id), @@ -30,24 +36,24 @@ func TestRun(name string, id int64, s Status, c Conclusion) Run { } } -var SuccessfulRun Run = TestRun("successful", 3, Completed, Success) -var FailedRun Run = TestRun("failed", 1234, Completed, Failure) +var SuccessfulRun Run = TestRun(3, Completed, Success) +var FailedRun Run = TestRun(1234, Completed, Failure) var TestRuns []Run = []Run{ - TestRun("timed out", 1, Completed, TimedOut), - TestRun("in progress", 2, InProgress, ""), + TestRun(1, Completed, TimedOut), + TestRun(2, InProgress, ""), SuccessfulRun, - TestRun("cancelled", 4, Completed, Cancelled), + TestRun(4, Completed, Cancelled), FailedRun, - TestRun("neutral", 6, Completed, Neutral), - TestRun("skipped", 7, Completed, Skipped), - TestRun("requested", 8, Requested, ""), - TestRun("queued", 9, Queued, ""), - TestRun("stale", 10, Completed, Stale), + TestRun(6, Completed, Neutral), + TestRun(7, Completed, Skipped), + TestRun(8, Requested, ""), + TestRun(9, Queued, ""), + TestRun(10, Completed, Stale), } var WorkflowRuns []Run = []Run{ - TestRun("in progress", 2, InProgress, ""), + TestRun(2, InProgress, ""), SuccessfulRun, FailedRun, } @@ -111,3 +117,8 @@ var FailedJobAnnotations []Annotation = []Annotation{ StartLine: 420, }, } + +var TestWorkflow workflowShared.Workflow = workflowShared.Workflow{ + Name: "CI", + ID: 123, +} diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 7be8f6458..abfdd0537 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -199,7 +199,7 @@ func runView(opts *ViewOptions) error { if err != nil { return fmt.Errorf("failed to get runs: %w", err) } - runID, err = shared.PromptForRun(cs, runs) + runID, err = shared.PromptForRun(cs, runs.WorkflowRuns) if err != nil { return err } @@ -466,15 +466,17 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { // This function takes a zip file of logs and a list of jobs. // Structure of zip file -// zip/ -// ├── jobname1/ -// │ ├── 1_stepname.txt -// │ ├── 2_anotherstepname.txt -// │ ├── 3_stepstepname.txt -// │ └── 4_laststepname.txt -// └── jobname2/ -// ├── 1_stepname.txt -// └── 2_somestepname.txt +// +// zip/ +// ├── jobname1/ +// │ ├── 1_stepname.txt +// │ ├── 2_anotherstepname.txt +// │ ├── 3_stepstepname.txt +// │ └── 4_laststepname.txt +// └── jobname2/ +// ├── 1_stepname.txt +// └── 2_somestepname.txt +// // It iterates through the list of jobs and trys to find the matching // log in the zip file. If the matching log is found it is attached // to the job. diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index b544bac97..ee1fca6ee 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -13,6 +13,7 @@ import ( "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/run/shared" + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -180,22 +181,25 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), httpmock.StringResponse(`{}`)) reg.Register( httpmock.GraphQL(`query PullRequestForRun`), httpmock.StringResponse(`{"data": { - "repository": { - "pullRequests": { - "nodes": [ - {"number": 2898, - "headRepository": { - "owner": { - "login": "OWNER" - }, - "name": "REPO"}} - ]}}}}`)) + "repository": { + "pullRequests": { + "nodes": [ + {"number": 2898, + "headRepository": { + "owner": { + "login": "OWNER" + }, + "name": "REPO"}} + ]}}}}`)) reg.Register( httpmock.REST("GET", "runs/3/jobs"), httpmock.JSONResponse(shared.JobsPayload{ @@ -207,7 +211,7 @@ func TestViewRun(t *testing.T) { httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), httpmock.JSONResponse([]shared.Annotation{})) }, - wantOut: "\n✓ trunk successful #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "exit status, failed run", @@ -219,6 +223,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/artifacts"), httpmock.StringResponse(`{}`)) @@ -236,7 +243,7 @@ func TestViewRun(t *testing.T) { httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), httpmock.JSONResponse(shared.FailedJobAnnotations)) }, - wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", + wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", wantErr: true, }, { @@ -263,10 +270,13 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "runs/3/jobs"), httpmock.JSONResponse(shared.JobsPayload{})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantOut: heredoc.Doc(` - ✓ trunk successful · 3 + ✓ trunk CI · 3 Triggered via push about 59 minutes ago JOBS @@ -307,8 +317,11 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), httpmock.JSONResponse([]shared.Annotation{})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "verbose", @@ -342,8 +355,11 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), httpmock.JSONResponse(shared.FailedJobAnnotations)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", + wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", }, { name: "prompts for choice, one job", @@ -373,6 +389,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), httpmock.JSONResponse([]shared.Annotation{})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -381,7 +407,7 @@ func TestViewRun(t *testing.T) { opts: &ViewOptions{ Prompt: true, }, - wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "interactive with log", @@ -410,6 +436,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -435,6 +471,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantOut: coolJobRunLogOutput, }, @@ -465,6 +504,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -496,6 +545,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantOut: expectedRunLogOutput, }, @@ -526,6 +578,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -551,6 +613,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantOut: quuxTheBarfLogOutput, }, @@ -581,6 +646,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -612,6 +687,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), httpmock.FileResponse("./fixtures/run_log.zip")) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantOut: quuxTheBarfLogOutput, }, @@ -625,12 +703,15 @@ func TestViewRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"), - httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, ""))) + httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, ""))) reg.Register( httpmock.REST("GET", "runs/2/jobs"), httpmock.JSONResponse(shared.JobsPayload{ Jobs: []shared.Job{}, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantErr: true, errMsg: "run 2 is still in progress; logs will be available when it is complete", @@ -652,7 +733,10 @@ func TestViewRun(t *testing.T) { })) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"), - httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, ""))) + httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, ""))) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, wantErr: true, errMsg: "job 20 is still in progress; logs will be available when it is complete", @@ -672,8 +756,11 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), httpmock.JSONResponse([]shared.Annotation{})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "interactive, multiple jobs, choose all jobs", @@ -707,6 +794,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), httpmock.JSONResponse(shared.FailedJobAnnotations)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -714,7 +811,7 @@ func TestViewRun(t *testing.T) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt as.StubOne(0) }, - wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "interactive, multiple jobs, choose specific jobs", @@ -742,6 +839,16 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), httpmock.JSONResponse([]shared.Annotation{})) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, askStubs: func(as *prompt.AskStubber) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt @@ -749,7 +856,7 @@ func TestViewRun(t *testing.T) { //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt as.StubOne(1) }, - wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", + wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", }, { name: "web run", @@ -762,6 +869,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, browsedURL: "https://github.com/runs/3", wantOut: "Opening github.com/runs/3 in your browser.\n", @@ -780,6 +890,9 @@ func TestViewRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, browsedURL: "https://github.com/jobs/10?check_suite_focus=true", wantOut: "Opening github.com/jobs/10 in your browser.\n", @@ -793,15 +906,18 @@ func TestViewRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"), - httpmock.JSONResponse(shared.TestRun("failed no job", 123, shared.Completed, shared.Failure))) + httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.Failure))) reg.Register( httpmock.REST("GET", "runs/123/jobs"), httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}})) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"), httpmock.StringResponse(`{}`)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "\nX trunk failed no job · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", + wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", }, { name: "hide job header, startup_failure", @@ -812,15 +928,18 @@ func TestViewRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"), - httpmock.JSONResponse(shared.TestRun("failed no job", 123, shared.Completed, shared.StartupFailure))) + httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.StartupFailure))) reg.Register( httpmock.REST("GET", "runs/123/jobs"), httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}})) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"), httpmock.StringResponse(`{}`)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "\nX trunk failed no job · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", + wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", }, } @@ -879,13 +998,14 @@ func TestViewRun(t *testing.T) { } // Structure of fixture zip file -// run log/ -// ├── cool job/ -// │ ├── 1_fob the barz.txt -// │ └── 2_barz the fob.txt -// └── sad job/ -// ├── 1_barf the quux.txt -// └── 2_quux the barf.txt +// +// run log/ +// ├── cool job/ +// │ ├── 1_fob the barz.txt +// │ └── 2_barz the fob.txt +// └── sad job/ +// ├── 1_barf the quux.txt +// └── 2_quux the barf.txt func Test_attachRunLog(t *testing.T) { tests := []struct { name string diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index 4e3d40c8a..a8ca005be 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -121,7 +121,7 @@ func watchRun(opts *WatchOptions) error { } if run.Status == shared.Completed { - fmt.Fprintf(opts.IO.Out, "Run %s (%s) has already completed with '%s'\n", cs.Bold(run.Name), cs.Cyanf("%d", run.ID), run.Conclusion) + fmt.Fprintf(opts.IO.Out, "Run %s (%s) has already completed with '%s'\n", cs.Bold(run.WorkflowName()), cs.Cyanf("%d", run.ID), run.Conclusion) if opts.ExitStatus && run.Conclusion != shared.Success { return cmdutil.SilentError } @@ -186,7 +186,7 @@ func watchRun(opts *WatchOptions) error { if opts.IO.IsStdoutTTY() { fmt.Fprintln(opts.IO.Out) - fmt.Fprintf(opts.IO.Out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.Name), id, run.Conclusion) + fmt.Fprintf(opts.IO.Out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.WorkflowName()), id, run.Conclusion) } if opts.ExitStatus && run.Conclusion != shared.Success { diff --git a/pkg/cmd/run/watch/watch_test.go b/pkg/cmd/run/watch/watch_test.go index cabf680ac..89c75e72c 100644 --- a/pkg/cmd/run/watch/watch_test.go +++ b/pkg/cmd/run/watch/watch_test.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/run/shared" + workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" @@ -98,13 +99,13 @@ func TestNewCmdWatch(t *testing.T) { func TestWatchRun(t *testing.T) { failedRunStubs := func(reg *httpmock.Registry) { - inProgressRun := shared.TestRun("more runs", 2, shared.InProgress, "") - completedRun := shared.TestRun("more runs", 2, shared.Completed, shared.Failure) + inProgressRun := shared.TestRunWithCommit(2, shared.InProgress, "", "commit2") + completedRun := shared.TestRun(2, shared.Completed, shared.Failure) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: []shared.Run{ - shared.TestRun("run", 1, shared.InProgress, ""), + shared.TestRunWithCommit(1, shared.InProgress, "", "commit1"), inProgressRun, }, })) @@ -128,15 +129,28 @@ func TestWatchRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), httpmock.JSONResponse(shared.FailedJobAnnotations)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) } successfulRunStubs := func(reg *httpmock.Registry) { - inProgressRun := shared.TestRun("more runs", 2, shared.InProgress, "") - completedRun := shared.TestRun("more runs", 2, shared.Completed, shared.Success) + inProgressRun := shared.TestRunWithCommit(2, shared.InProgress, "", "commit2") + completedRun := shared.TestRun(2, shared.Completed, shared.Success) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: []shared.Run{ - shared.TestRun("run", 1, shared.InProgress, ""), + shared.TestRunWithCommit(1, shared.InProgress, "", "commit1"), inProgressRun, }, })) @@ -163,6 +177,19 @@ func TestWatchRun(t *testing.T) { shared.SuccessfulJob, }, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) } tests := []struct { @@ -184,8 +211,11 @@ func TestWatchRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "Run failed (1234) has already completed with 'failure'\n", + wantOut: "Run CI (1234) has already completed with 'failure'\n", }, { name: "already completed, exit status", @@ -197,8 +227,11 @@ func TestWatchRun(t *testing.T) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), + httpmock.JSONResponse(shared.TestWorkflow)) }, - wantOut: "Run failed (1234) has already completed with 'failure'\n", + wantOut: "Run CI (1234) has already completed with 'failure'\n", wantErr: true, errMsg: "SilentError", }, @@ -219,6 +252,13 @@ func TestWatchRun(t *testing.T) { shared.SuccessfulRun, }, })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), + httpmock.JSONResponse(workflowShared.WorkflowsPayload{ + Workflows: []workflowShared.Workflow{ + shared.TestWorkflow, + }, + })) }, }, { @@ -231,10 +271,10 @@ func TestWatchRun(t *testing.T) { httpStubs: successfulRunStubs, askStubs: func(as *prompt.AskStubber) { as.StubPrompt("Select a workflow run"). - AssertOptions([]string{"* cool commit, run (trunk) Feb 23, 2021", "* cool commit, more runs (trunk) Feb 23, 2021"}). - AnswerWith("* cool commit, more runs (trunk) Feb 23, 2021") + AssertOptions([]string{"* commit1, CI (trunk) Feb 23, 2021", "* commit2, CI (trunk) Feb 23, 2021"}). + AnswerWith("* commit2, CI (trunk) Feb 23, 2021") }, - wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\x1b[?1049l✓ trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\n✓ Run more runs (2) completed with 'success'\n", + wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\x1b[?1049l✓ trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\n✓ Run CI (2) completed with 'success'\n", }, { name: "exit status respected", @@ -247,10 +287,10 @@ func TestWatchRun(t *testing.T) { httpStubs: failedRunStubs, askStubs: func(as *prompt.AskStubber) { as.StubPrompt("Select a workflow run"). - AssertOptions([]string{"* cool commit, run (trunk) Feb 23, 2021", "* cool commit, more runs (trunk) Feb 23, 2021"}). - AnswerWith("* cool commit, more runs (trunk) Feb 23, 2021") + AssertOptions([]string{"* commit1, CI (trunk) Feb 23, 2021", "* commit2, CI (trunk) Feb 23, 2021"}). + AnswerWith("* commit2, CI (trunk) Feb 23, 2021") }, - wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk more runs · 2\nTriggered via push about 59 minutes ago\n\n\x1b[?1049lX trunk more runs · 2\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nX Run more runs (2) completed with 'failure'\n", + wantOut: "\x1b[?1049h\x1b[0;0H\x1b[JRefreshing run status every 0 seconds. Press Ctrl+C to quit.\n\n* trunk CI · 2\nTriggered via push about 59 minutes ago\n\n\x1b[?1049lX trunk CI · 2\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nX Run CI (2) completed with 'failure'\n", wantErr: true, errMsg: "SilentError", }, diff --git a/pkg/cmd/workflow/disable/disable_test.go b/pkg/cmd/workflow/disable/disable_test.go index ea6016092..4bbf766e7 100644 --- a/pkg/cmd/workflow/disable/disable_test.go +++ b/pkg/cmd/workflow/disable/disable_test.go @@ -132,9 +132,6 @@ func TestDisableRun(t *testing.T) { }, tty: true, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -157,9 +154,6 @@ func TestDisableRun(t *testing.T) { }, tty: true, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/another%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -216,9 +210,6 @@ func TestDisableRun(t *testing.T) { Selector: "a workflow", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ diff --git a/pkg/cmd/workflow/enable/enable_test.go b/pkg/cmd/workflow/enable/enable_test.go index 7e0f29031..b626f794d 100644 --- a/pkg/cmd/workflow/enable/enable_test.go +++ b/pkg/cmd/workflow/enable/enable_test.go @@ -132,9 +132,6 @@ func TestEnableRun(t *testing.T) { }, tty: true, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/terrible%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -158,9 +155,6 @@ func TestEnableRun(t *testing.T) { }, tty: true, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -187,9 +181,6 @@ func TestEnableRun(t *testing.T) { }, tty: true, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20inactivity%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -242,9 +233,6 @@ func TestEnableRun(t *testing.T) { Selector: "terrible workflow", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/terrible%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -267,9 +255,6 @@ func TestEnableRun(t *testing.T) { Selector: "a disabled inactivity workflow", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20inactivity%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ @@ -291,9 +276,6 @@ func TestEnableRun(t *testing.T) { Selector: "a disabled workflow", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/a%20disabled%20workflow"), - httpmock.StatusStringResponse(404, "not found")) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(shared.WorkflowsPayload{ diff --git a/pkg/cmd/workflow/shared/shared.go b/pkg/cmd/workflow/shared/shared.go index 4a276f487..ae5541824 100644 --- a/pkg/cmd/workflow/shared/shared.go +++ b/pkg/cmd/workflow/shared/shared.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "path" + "strconv" "strings" "github.com/AlecAivazis/survey/v2" @@ -117,32 +118,33 @@ func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowSta return &filtered[selected], nil } +// FindWorkflow looks up a workflow either by numeric database ID, file name, or its Name field func FindWorkflow(client *api.Client, repo ghrepo.Interface, workflowSelector string, states []WorkflowState) ([]Workflow, error) { if workflowSelector == "" { return nil, errors.New("empty workflow selector") } - workflow, err := getWorkflowByID(client, repo, workflowSelector) - if err == nil { - return []Workflow{*workflow}, nil - } else { - var httpErr api.HTTPError - if !errors.As(err, &httpErr) || httpErr.StatusCode != 404 { + if _, err := strconv.Atoi(workflowSelector); err == nil || strings.HasSuffix(workflowSelector, ".yml") { + workflow, err := getWorkflowByID(client, repo, workflowSelector) + if err != nil { return nil, err } + return []Workflow{*workflow}, nil } return getWorkflowsByName(client, repo, workflowSelector, states) } +func GetWorkflow(client *api.Client, repo ghrepo.Interface, workflowID int64) (*Workflow, error) { + return getWorkflowByID(client, repo, strconv.FormatInt(workflowID, 10)) +} + +// ID can be either a numeric database ID or the workflow file name func getWorkflowByID(client *api.Client, repo ghrepo.Interface, ID string) (*Workflow, error) { var workflow Workflow - err := client.REST(repo.RepoHost(), "GET", - fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID)), - nil, &workflow) - - if err != nil { + path := fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID)) + if err := client.REST(repo.RepoHost(), "GET", path, nil, &workflow); err != nil { return nil, err } @@ -154,24 +156,17 @@ func getWorkflowsByName(client *api.Client, repo ghrepo.Interface, name string, if err != nil { return nil, fmt.Errorf("couldn't fetch workflows for %s: %w", ghrepo.FullName(repo), err) } - filtered := []Workflow{} + var filtered []Workflow for _, workflow := range workflows { - desiredState := false - for _, state := range states { - if workflow.State == state { - desiredState = true - break - } - } - - if !desiredState { + if !strings.EqualFold(workflow.Name, name) { continue } - - // TODO consider fuzzy or prefix match - if strings.EqualFold(workflow.Name, name) { - filtered = append(filtered, workflow) + for _, state := range states { + if workflow.State == state { + filtered = append(filtered, workflow) + break + } } } diff --git a/pkg/cmd/workflow/view/http.go b/pkg/cmd/workflow/view/http.go deleted file mode 100644 index c85da2c97..000000000 --- a/pkg/cmd/workflow/view/http.go +++ /dev/null @@ -1,31 +0,0 @@ -package view - -import ( - "fmt" - - "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/internal/ghrepo" - runShared "github.com/cli/cli/v2/pkg/cmd/run/shared" - "github.com/cli/cli/v2/pkg/cmd/workflow/shared" -) - -type workflowRuns struct { - Total int - Runs []runShared.Run -} - -func getWorkflowRuns(client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) (workflowRuns, error) { - var wr workflowRuns - var result runShared.RunsPayload - path := fmt.Sprintf("repos/%s/actions/workflows/%d/runs?per_page=%d&page=%d", ghrepo.FullName(repo), workflow.ID, 5, 1) - - err := client.REST(repo.RepoHost(), "GET", path, nil, &result) - if err != nil { - return wr, err - } - - wr.Total = result.TotalCount - wr.Runs = append(wr.Runs, result.WorkflowRuns...) - - return wr, nil -} diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index a03592bbe..7d087c989 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -195,7 +195,10 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Inte } func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) error { - wr, err := getWorkflowRuns(client, repo, workflow) + wr, err := runShared.GetRuns(client, repo, &runShared.FilterOptions{ + WorkflowID: workflow.ID, + WorkflowName: workflow.Name, + }, 5) if err != nil { return fmt.Errorf("failed to get runs: %w", err) } @@ -210,13 +213,13 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa fmt.Fprintf(out, "ID: %s\n\n", cs.Cyanf("%d", workflow.ID)) // Runs - fmt.Fprintf(out, "Total runs %d\n", wr.Total) + fmt.Fprintf(out, "Total runs %d\n", wr.TotalCount) - if wr.Total != 0 { + if wr.TotalCount != 0 { fmt.Fprintln(out, "Recent runs") } - for _, run := range wr.Runs { + for _, run := range wr.WorkflowRuns { if opts.Raw { tp.AddField(string(run.Status), nil, nil) tp.AddField(string(run.Conclusion), nil, nil) @@ -225,9 +228,9 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa tp.AddField(symbol, nil, symbolColor) } - tp.AddField(run.CommitMsg(), nil, cs.Bold) + tp.AddField(run.Title(), nil, cs.Bold) - tp.AddField(run.Name, nil, nil) + tp.AddField(run.WorkflowName(), nil, nil) tp.AddField(run.HeadBranch, nil, cs.Bold) tp.AddField(string(run.Event), nil, nil) @@ -248,7 +251,7 @@ func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interfa fmt.Fprintln(out) // Footer - if wr.Total != 0 { + if wr.TotalCount != 0 { fmt.Fprintf(out, "To see more runs for this workflow, try: gh run list --workflow %s\n", filename) } fmt.Fprintf(out, "To see the YAML for this workflow, try: gh workflow view %s --yaml\n", filename) diff --git a/pkg/cmd/workflow/view/view_test.go b/pkg/cmd/workflow/view/view_test.go index 412f9e755..9be264d92 100644 --- a/pkg/cmd/workflow/view/view_test.go +++ b/pkg/cmd/workflow/view/view_test.go @@ -176,10 +176,10 @@ func TestViewRun(t *testing.T) { Total runs 10 Recent runs - X cool commit timed out trunk push 1 - * cool commit in progress trunk push 2 - ✓ cool commit successful trunk push 3 - X cool commit cancelled trunk push 4 + X cool commit a workflow trunk push 1 + * cool commit a workflow trunk push 2 + ✓ cool commit a workflow trunk push 3 + X cool commit a workflow trunk push 4 To see more runs for this workflow, try: gh run list --workflow flow.yml To see the YAML for this workflow, try: gh workflow view flow.yml --yaml