From 289469565281dd8834621e303f23ec5aaaf00d9e Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:11:03 +0800 Subject: [PATCH 01/13] feat: add attempt flag to run view --- pkg/cmd/run/view/view.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 895823ade..43c7b351c 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -74,6 +74,7 @@ type ViewOptions struct { Log bool LogFailed bool Web bool + Attempt uint64 Prompt bool Exporter cmdutil.Exporter @@ -93,7 +94,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "view []", Short: "View a summary of a workflow run", - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(2), Example: heredoc.Doc(` # Interactively select a run to view, optionally selecting a single job $ gh run view @@ -101,6 +102,9 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman # View a specific run $ gh run view 12345 + # View a specific run with specific attempt number + $ gh run view 12345 --attempt 3 + # View a specific job within a run $ gh run view --job 456789 @@ -153,6 +157,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd.Flags().BoolVar(&opts.Log, "log", false, "View full log for either a run or specific job") cmd.Flags().BoolVar(&opts.LogFailed, "log-failed", false, "View the log for any failed steps in a run or specific job") cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open run in the browser") + cmd.Flags().Uint64VarP(&opts.Attempt, "attempt", "a", 0, "The attempt number of the workflow run") cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.SingleRunFields) return cmd @@ -172,6 +177,7 @@ func runView(opts *ViewOptions) error { jobID := opts.JobID runID := opts.RunID + attempt := opts.Attempt var selectedJob *shared.Job var run *shared.Run var jobs []shared.Job @@ -206,7 +212,7 @@ func runView(opts *ViewOptions) error { } opts.IO.StartProgressIndicator() - run, err = shared.GetRun(client, repo, runID) + run, err = shared.GetRun(client, repo, runID, attempt) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get run: %w", err) From bd0f535dea139d2586744396e806f65ff5d2c52c Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:11:19 +0800 Subject: [PATCH 02/13] feat: pass 0 to fetch latest attempt --- pkg/cmd/run/cancel/cancel.go | 2 +- pkg/cmd/run/rerun/rerun.go | 2 +- pkg/cmd/run/watch/watch.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run/cancel/cancel.go b/pkg/cmd/run/cancel/cancel.go index 39891986c..ca4521b1d 100644 --- a/pkg/cmd/run/cancel/cancel.go +++ b/pkg/cmd/run/cancel/cancel.go @@ -95,7 +95,7 @@ func runCancel(opts *CancelOptions) error { } } } else { - run, err = shared.GetRun(client, repo, runID) + run, err = shared.GetRun(client, repo, runID, 0) if err != nil { var httpErr api.HTTPError if errors.As(err, &httpErr) { diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index f439a2304..2522e632c 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -139,7 +139,7 @@ func runRerun(opts *RerunOptions) error { } } else { opts.IO.StartProgressIndicator() - run, err := shared.GetRun(client, repo, runID) + run, err := shared.GetRun(client, repo, runID, 0) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get run: %w", err) diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index d56c42405..0d31f8299 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -114,7 +114,7 @@ func watchRun(opts *WatchOptions) error { } } } else { - run, err = shared.GetRun(client, repo, runID) + run, err = shared.GetRun(client, repo, runID, 0) if err != nil { return fmt.Errorf("failed to get run: %w", err) } @@ -201,7 +201,7 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo var err error - run, err = shared.GetRun(client, repo, fmt.Sprintf("%d", run.ID)) + run, err = shared.GetRun(client, repo, fmt.Sprintf("%d", run.ID), 0) if err != nil { return nil, fmt.Errorf("failed to get run: %w", err) } From 9a0f7916c3387d983e9b23a83c98da48efe40a21 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:11:42 +0800 Subject: [PATCH 03/13] feat: update GetRun path based on attempt --- pkg/cmd/run/shared/shared.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index a1a71c4f9..cfee75705 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -449,10 +449,15 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { return fmt.Sprintf("%d", runs[selected].ID), nil } -func GetRun(client *api.Client, repo ghrepo.Interface, runID string) (*Run, error) { +func GetRun(client *api.Client, repo ghrepo.Interface, runID string, attempt uint64) (*Run, error) { var result Run + var path string - path := fmt.Sprintf("repos/%s/actions/runs/%s?exclude_pull_requests=true", ghrepo.FullName(repo), runID) + if attempt == 0 { + path = fmt.Sprintf("repos/%s/actions/runs/%s?exclude_pull_requests=true", ghrepo.FullName(repo), runID) + } else { + path = fmt.Sprintf("repos/%s/actions/runs/%s/attempts/%d?exclude_pull_requests=true", ghrepo.FullName(repo), runID, attempt) + } err := client.REST(repo.RepoHost(), "GET", path, nil, &result) if err != nil { From 343e7300168ed4c008b2b952cba741e37d24a6fd Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:38:05 +0800 Subject: [PATCH 04/13] feat: pass attempt to RenderRunHeader --- pkg/cmd/run/view/view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 43c7b351c..10ed4919f 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -324,7 +324,7 @@ func runView(opts *ViewOptions) error { out := opts.IO.Out fmt.Fprintln(out) - fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber)) + fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber, attempt)) fmt.Fprintln(out) if len(jobs) == 0 && run.Conclusion == shared.Failure || run.Conclusion == shared.StartupFailure { From baaa5c3184753ecbd0d15ad75572e322ca72e535 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:38:24 +0800 Subject: [PATCH 05/13] feat: add attemptLabel to RenderRunHeader --- pkg/cmd/run/shared/presentation.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index 900bd07a1..21ee15c25 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -7,14 +7,21 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" ) -func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string) string { +func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string, attempt uint64) string { title := fmt.Sprintf("%s %s%s", cs.Bold(run.HeadBranch), run.WorkflowName(), prNumber) symbol, symbolColor := Symbol(cs, run.Status, run.Conclusion) id := cs.Cyanf("%d", run.ID) + var attemptLabel string + if attempt == 0 { + attemptLabel = "Latest Attempt" + } else { + attemptLabel = fmt.Sprintf("Attempt #%d", attempt) + } + header := "" - header += fmt.Sprintf("%s %s · %s\n", symbolColor(symbol), title, id) + header += fmt.Sprintf("%s %s · %s (%s)\n", symbolColor(symbol), title, id, attemptLabel) header += fmt.Sprintf("Triggered via %s %s", run.Event, ago) return header From 6c663427425390ab79e60d682d5437993ecf1901 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 14:38:53 +0800 Subject: [PATCH 06/13] feat: pass latest attempt to RenderRunHeader for watch --- pkg/cmd/run/watch/watch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index 0d31f8299..05b207693 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -236,7 +236,7 @@ func renderRun(out io.Writer, opts WatchOptions, client *api.Client, repo ghrepo return nil, fmt.Errorf("failed to get annotations: %w", annotationErr) } - fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber)) + fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber, 0)) fmt.Fprintln(out) if len(jobs) == 0 { From 3796b6d6e8641170daed89a2acf71678100d8b95 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 16:10:42 +0800 Subject: [PATCH 07/13] feat: append attempt number in returned run url --- pkg/cmd/run/shared/shared.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index cfee75705..90aae6d9d 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -464,6 +464,10 @@ func GetRun(client *api.Client, repo ghrepo.Interface, runID string, attempt uin return nil, err } + if attempt > 0 { + result.URL += fmt.Sprintf("/attempts/%d", attempt) + } + // Set name to workflow name workflow, err := workflowShared.GetWorkflow(client, repo, result.WorkflowID) if err != nil { From 897a2aa53f3d65fd15974c7f652a02f656240693 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 16:20:57 +0800 Subject: [PATCH 08/13] refactor: don't show latest attempt --- pkg/cmd/run/shared/presentation.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/run/shared/presentation.go b/pkg/cmd/run/shared/presentation.go index 21ee15c25..2ec149729 100644 --- a/pkg/cmd/run/shared/presentation.go +++ b/pkg/cmd/run/shared/presentation.go @@ -13,15 +13,13 @@ func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string, a symbol, symbolColor := Symbol(cs, run.Status, run.Conclusion) id := cs.Cyanf("%d", run.ID) - var attemptLabel string - if attempt == 0 { - attemptLabel = "Latest Attempt" - } else { - attemptLabel = fmt.Sprintf("Attempt #%d", attempt) + attemptLabel := "" + if attempt > 0 { + attemptLabel = fmt.Sprintf(" (Attempt #%d)", attempt) } header := "" - header += fmt.Sprintf("%s %s · %s (%s)\n", symbolColor(symbol), title, id, attemptLabel) + header += fmt.Sprintf("%s %s · %s%s\n", symbolColor(symbol), title, id, attemptLabel) header += fmt.Sprintf("Triggered via %s %s", run.Event, ago) return header From a4c77ffd92489bda7c1505f9055cb230f25f6691 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 19:12:21 +0800 Subject: [PATCH 09/13] feat: include attempt in getRunLog --- pkg/cmd/run/view/view.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 10ed4919f..9470b0001 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -277,7 +277,7 @@ func runView(opts *ViewOptions) error { } opts.IO.StartProgressIndicator() - runLogZip, err := getRunLog(opts.RunLogCache, httpClient, repo, run) + runLogZip, err := getRunLog(opts.RunLogCache, httpClient, repo, run, attempt) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get run log: %w", err) @@ -431,13 +431,19 @@ func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) { return resp.Body, nil } -func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface, run *shared.Run) (*zip.ReadCloser, error) { +func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface, run *shared.Run, attempt uint64) (*zip.ReadCloser, error) { filename := fmt.Sprintf("run-log-%d-%d.zip", run.ID, run.StartedTime().Unix()) filepath := filepath.Join(os.TempDir(), "gh-cli-cache", filename) if !cache.Exists(filepath) { // Run log does not exist in cache so retrieve and store it - logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs", - ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID) + var logURL string + if attempt == 0 { + logURL = fmt.Sprintf("%srepos/%s/actions/runs/%d/logs", + ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID) + } else { + logURL = fmt.Sprintf("%srepos/%s/actions/runs/%d/attempts/%d/logs", + ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID, attempt) + } resp, err := getLog(httpClient, logURL) if err != nil { From 0fa168b80bcf9dc8cd99c9e410c12cc016f21ece Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sun, 26 Mar 2023 19:12:40 +0800 Subject: [PATCH 10/13] feat: add tests for attempt flag --- pkg/cmd/run/view/view_test.go | 249 ++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index ee1fca6ee..99a9ef30e 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -117,6 +117,14 @@ func TestNewCmdView(t *testing.T) { JobID: "4567", }, }, + { + name: "run id with attempt", + cli: "1234 --attempt 2", + wants: ViewOptions{ + RunID: "1234", + Attempt: 2, + }, + }, } for _, tt := range tests { @@ -154,6 +162,7 @@ func TestNewCmdView(t *testing.T) { assert.Equal(t, tt.wants.Prompt, gotOpts.Prompt) assert.Equal(t, tt.wants.ExitStatus, gotOpts.ExitStatus) assert.Equal(t, tt.wants.Verbose, gotOpts.Verbose) + assert.Equal(t, tt.wants.Attempt, gotOpts.Attempt) }) } } @@ -213,6 +222,50 @@ func TestViewRun(t *testing.T) { }, 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: "associate with PR with attempt", + tty: true, + opts: &ViewOptions{ + RunID: "3", + Attempt: 3, + Prompt: false, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/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"}} + ]}}}}`)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), + httpmock.JSONResponse([]shared.Annotation{})) + }, + wantOut: "\n✓ trunk CI #2898 · 3 (Attempt #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/attempts/3\n", + }, { name: "exit status, failed run", opts: &ViewOptions{ @@ -291,6 +344,52 @@ func TestViewRun(t *testing.T) { View this run on GitHub: https://github.com/runs/3 `), }, + { + name: "with artifacts and attempt", + opts: &ViewOptions{ + RunID: "3", + Attempt: 3, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), + httpmock.JSONResponse(map[string][]shared.Artifact{ + "artifacts": { + shared.Artifact{Name: "artifact-1", Expired: false}, + shared.Artifact{Name: "artifact-2", Expired: true}, + shared.Artifact{Name: "artifact-3", Expired: false}, + }, + })) + reg.Register( + httpmock.GraphQL(`query PullRequestForRun`), + httpmock.StringResponse(``)) + 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 CI · 3 (Attempt #3) + Triggered via push about 59 minutes ago + + JOBS + + + ARTIFACTS + artifact-1 + artifact-2 (expired) + artifact-3 + + For more information about a job, try: gh run view --job= + View this run on GitHub: https://github.com/runs/3/attempts/3 + `), + }, { name: "exit status, successful run", opts: &ViewOptions{ @@ -323,6 +422,39 @@ func TestViewRun(t *testing.T) { }, 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: "exit status, successful run, with attempt", + opts: &ViewOptions{ + RunID: "3", + Attempt: 3, + ExitStatus: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), + httpmock.StringResponse(`{}`)) + reg.Register( + httpmock.GraphQL(`query PullRequestForRun`), + httpmock.StringResponse(``)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + }, + })) + 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 CI · 3 (Attempt #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/attempts/3\n", + }, { name: "verbose", tty: true, @@ -455,6 +587,53 @@ func TestViewRun(t *testing.T) { }, wantOut: coolJobRunLogOutput, }, + { + name: "interactive with log and attempt", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Attempt: 3, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "runs/3/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + shared.FailedJob, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/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 + as.StubOne(2) + //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt + as.StubOne(1) + }, + wantOut: coolJobRunLogOutput, + }, { name: "noninteractive with log", opts: &ViewOptions{ @@ -477,6 +656,29 @@ func TestViewRun(t *testing.T) { }, wantOut: coolJobRunLogOutput, }, + { + name: "noninteractive with log and attempt", + opts: &ViewOptions{ + JobID: "10", + Attempt: 3, + Log: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), + httpmock.JSONResponse(shared.SuccessfulJob)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/3"), + httpmock.JSONResponse(shared.SuccessfulRun)) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/attempts/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, + }, { name: "interactive with run log", tty: true, @@ -597,6 +799,53 @@ func TestViewRun(t *testing.T) { }, wantOut: quuxTheBarfLogOutput, }, + { + name: "interactive with log-failed with attempt", + tty: true, + opts: &ViewOptions{ + Prompt: true, + Attempt: 3, + LogFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), + httpmock.JSONResponse(shared.RunsPayload{ + WorkflowRuns: shared.TestRuns, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/attempts/3"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("GET", "runs/1234/jobs"), + httpmock.JSONResponse(shared.JobsPayload{ + Jobs: []shared.Job{ + shared.SuccessfulJob, + shared.FailedJob, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/attempts/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 + as.StubOne(4) + //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt + as.StubOne(2) + }, + wantOut: quuxTheBarfLogOutput, + }, { name: "noninteractive with log-failed", opts: &ViewOptions{ From cc554135e826a1acfe49202c162d457377d946fe Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 28 Mar 2023 21:16:04 +0800 Subject: [PATCH 11/13] fix: change MaximumNArgs from 2 to 1 in run view.go --- pkg/cmd/run/view/view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 9470b0001..b276cce77 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -94,7 +94,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "view []", Short: "View a summary of a workflow run", - Args: cobra.MaximumNArgs(2), + Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` # Interactively select a run to view, optionally selecting a single job $ gh run view From 48ed914d585e43ece728125d902d0606aefcae0c Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 28 Mar 2023 21:21:11 +0800 Subject: [PATCH 12/13] refactor: set the default case first --- pkg/cmd/run/shared/shared.go | 7 +++---- pkg/cmd/run/view/view.go | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 90aae6d9d..87bbb907c 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -451,11 +451,10 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { func GetRun(client *api.Client, repo ghrepo.Interface, runID string, attempt uint64) (*Run, error) { var result Run - var path string - if attempt == 0 { - path = fmt.Sprintf("repos/%s/actions/runs/%s?exclude_pull_requests=true", ghrepo.FullName(repo), runID) - } else { + path := fmt.Sprintf("repos/%s/actions/runs/%s?exclude_pull_requests=true", ghrepo.FullName(repo), runID) + + if attempt > 0 { path = fmt.Sprintf("repos/%s/actions/runs/%s/attempts/%d?exclude_pull_requests=true", ghrepo.FullName(repo), runID, attempt) } diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index b276cce77..34c5fcbc7 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -436,11 +436,10 @@ func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface filepath := filepath.Join(os.TempDir(), "gh-cli-cache", filename) if !cache.Exists(filepath) { // Run log does not exist in cache so retrieve and store it - var logURL string - if attempt == 0 { - logURL = fmt.Sprintf("%srepos/%s/actions/runs/%d/logs", - ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID) - } else { + logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs", + ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID) + + if attempt > 0 { logURL = fmt.Sprintf("%srepos/%s/actions/runs/%d/attempts/%d/logs", ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), run.ID, attempt) } From 0dd8261d2b12d2e0f11c484fcc28f4053e35e1f3 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Tue, 28 Mar 2023 21:25:42 +0800 Subject: [PATCH 13/13] refactor: use net/url JoinPath --- pkg/cmd/run/shared/shared.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index 87bbb907c..db347eee5 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -464,7 +464,10 @@ func GetRun(client *api.Client, repo ghrepo.Interface, runID string, attempt uin } if attempt > 0 { - result.URL += fmt.Sprintf("/attempts/%d", attempt) + result.URL, err = url.JoinPath(result.URL, fmt.Sprintf("/attempts/%d", attempt)) + if err != nil { + return nil, err + } } // Set name to workflow name