From ff6c7b925f99250b33618ad6249cc75177c76001 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 10 Feb 2022 13:53:55 -0800 Subject: [PATCH 1/7] Add flag to rerun only failed jobs in a workflow run --- pkg/cmd/run/rerun/rerun.go | 19 +++++++++++++++--- pkg/cmd/run/rerun/rerun_test.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 9788a3061..98cfa7060 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -18,7 +18,8 @@ type RerunOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) - RunID string + RunID string + OnlyFailed bool Prompt bool } @@ -52,6 +53,8 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } + cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + return cmd } @@ -98,7 +101,12 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to get run: %w", err) } - path := fmt.Sprintf("repos/%s/actions/runs/%d/rerun", ghrepo.FullName(repo), run.ID) + runVerb := "rerun" + if opts.OnlyFailed { + runVerb = "rerun-failed-jobs" + } + + path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb) err = client.REST(repo.RepoHost(), "POST", path, nil, nil) if err != nil { @@ -111,8 +119,13 @@ func runRerun(opts *RerunOptions) error { if opts.IO.CanPrompt() { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Requested rerun of run %s\n", + onlyFailedMsg := "" + if opts.OnlyFailed { + onlyFailedMsg = "(failed jobs) " + } + fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", cs.SuccessIcon(), + onlyFailedMsg, cs.Cyanf("%d", run.ID)) } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 11362ce4b..3fcf68ff3 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -50,6 +50,23 @@ func TestNewCmdRerun(t *testing.T) { RunID: "1234", }, }, + { + name: "failed arg nontty", + cli: "4321 --failed", + wants: RerunOptions{ + RunID: "4321", + OnlyFailed: true, + }, + }, + { + name: "failed arg", + tty: true, + cli: "--failed", + wants: RerunOptions{ + Prompt: true, + OnlyFailed: true, + }, + }, } for _, tt := range tests { @@ -117,6 +134,23 @@ func TestRerun(t *testing.T) { }, wantOut: "✓ Requested rerun of run 1234\n", }, + { + name: "arg including onlyFailed", + tty: true, + opts: &RerunOptions{ + RunID: "1234", + OnlyFailed: true, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), + httpmock.JSONResponse(shared.FailedRun)) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), + httpmock.StringResponse("{}")) + }, + wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", + }, { name: "prompt", tty: true, From c38ca830be5b34dad8a457076ac61fa203e33d0d Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:10:22 -0800 Subject: [PATCH 2/7] Extract shared.GetJob so we can use in rerun too --- pkg/cmd/run/shared/shared.go | 12 ++++++++++++ pkg/cmd/run/view/view.go | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index b015ecc0e..81cbcc8f4 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -307,6 +307,18 @@ func GetJobs(client *api.Client, repo ghrepo.Interface, run Run) ([]Job, error) return result.Jobs, nil } +func GetJob(client *api.Client, repo ghrepo.Interface, jobID string) (*Job, error) { + path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID) + + var result Job + err := client.REST(repo.RepoHost(), "GET", path, nil, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) { var selected int now := time.Now() diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index f41f37801..63518a632 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -183,7 +183,7 @@ func runView(opts *ViewOptions) error { if jobID != "" { opts.IO.StartProgressIndicator() - selectedJob, err = getJob(client, repo, jobID) + selectedJob, err = shared.GetJob(client, repo, jobID) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to get job: %w", err) @@ -395,18 +395,6 @@ func runView(opts *ViewOptions) error { return nil } -func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) { - path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID) - - var result shared.Job - err := client.REST(repo.RepoHost(), "GET", path, nil, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) { req, err := http.NewRequest("GET", logURL, nil) if err != nil { From bf5801e646221e93784ae1db76a75989088b154a Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:49:08 -0800 Subject: [PATCH 3/7] Implement `--job` for rerunning a specific actions job --- pkg/cmd/run/rerun/rerun.go | 112 ++++++++++++++++++++++++-------- pkg/cmd/run/rerun/rerun_test.go | 45 ++++++++++++- 2 files changed, 129 insertions(+), 28 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 98cfa7060..21a3c31d1 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -20,6 +20,7 @@ type RerunOptions struct { RunID string OnlyFailed bool + JobID string Prompt bool } @@ -38,12 +39,22 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm // support `-R, --repo` override opts.BaseRepo = f.BaseRepo - if len(args) > 0 { + if len(args) == 0 && opts.JobID == "" { + if !opts.IO.CanPrompt() { + return cmdutil.FlagErrorf("run or job ID required when not running interactively") + } else { + opts.Prompt = true + } + } else if len(args) > 0 { opts.RunID = args[0] - } else if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("run ID required when not running interactively") - } else { - opts.Prompt = true + } + + if opts.RunID != "" && opts.JobID != "" { + opts.RunID = "" + if opts.IO.CanPrompt() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) + } } if runF != nil { @@ -54,6 +65,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") return cmd } @@ -70,10 +82,23 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to determine base repo: %w", err) } + cs := opts.IO.ColorScheme() + runID := opts.RunID + jobID := opts.JobID + var selectedJob *shared.Job + + if jobID != "" { + opts.IO.StartProgressIndicator() + selectedJob, err = shared.GetJob(client, repo, jobID) + opts.IO.StopProgressIndicator() + if err != nil { + return fmt.Errorf("failed to get job: %w", err) + } + runID = fmt.Sprintf("%d", selectedJob.RunID) + } if opts.Prompt { - cs := opts.IO.ColorScheme() runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { if run.Status != shared.Completed { return false @@ -94,40 +119,73 @@ func runRerun(opts *RerunOptions) error { } } - opts.IO.StartProgressIndicator() - run, err := shared.GetRun(client, repo, runID) - opts.IO.StopProgressIndicator() - if err != nil { - return fmt.Errorf("failed to get run: %w", err) + if opts.JobID != "" { + err = rerunJob(client, repo, selectedJob) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n", + cs.SuccessIcon(), + cs.Cyanf("%d", selectedJob.ID), + cs.Cyanf("%d", selectedJob.RunID)) + } + } else { + opts.IO.StartProgressIndicator() + run, err := shared.GetRun(client, repo, runID) + opts.IO.StopProgressIndicator() + if err != nil { + return fmt.Errorf("failed to get run: %w", err) + } + + err = rerunRun(client, repo, run, opts.OnlyFailed) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + onlyFailedMsg := "" + if opts.OnlyFailed { + onlyFailedMsg = "(failed jobs) " + } + fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", + cs.SuccessIcon(), + onlyFailedMsg, + cs.Cyanf("%d", run.ID)) + } } + return nil +} + +func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed bool) error { runVerb := "rerun" - if opts.OnlyFailed { + if onlyFailed { runVerb = "rerun-failed-jobs" } path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb) - err = client.REST(repo.RepoHost(), "POST", path, nil, nil) + err := client.REST(repo.RepoHost(), "POST", path, nil, nil) if err != nil { var httpError api.HTTPError if errors.As(err, &httpError) && httpError.StatusCode == 403 { - return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken.", run.ID) + return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID) + } + return fmt.Errorf("failed to rerun: %w", err) + } + return nil +} + +func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job) error { + path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID) + + err := client.REST(repo.RepoHost(), "POST", path, nil, nil) + if err != nil { + var httpError api.HTTPError + if errors.As(err, &httpError) && httpError.StatusCode == 403 { + return fmt.Errorf("job %d cannot be rerun", job.ID) } return fmt.Errorf("failed to rerun: %w", err) } - - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - onlyFailedMsg := "" - if opts.OnlyFailed { - onlyFailedMsg = "(failed jobs) " - } - fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n", - cs.SuccessIcon(), - onlyFailedMsg, - cs.Cyanf("%d", run.ID)) - } - return nil } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 3fcf68ff3..094425290 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -67,6 +67,33 @@ func TestNewCmdRerun(t *testing.T) { OnlyFailed: true, }, }, + { + name: "with arg job", + tty: true, + cli: "--job 1234", + wants: RerunOptions{ + JobID: "1234", + }, + }, + { + name: "with args job and runID ignores runID", + tty: true, + cli: "1234 --job 5678", + wants: RerunOptions{ + JobID: "5678", + }, + }, + { + name: "with arg job with no ID fails", + tty: true, + cli: "--job", + wantsErr: true, + }, + { + name: "with arg job with no ID no tty fails", + cli: "--job", + wantsErr: true, + }, } for _, tt := range tests { @@ -151,6 +178,22 @@ func TestRerun(t *testing.T) { }, wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", }, + { + name: "arg including a specific job", + tty: true, + opts: &RerunOptions{ + JobID: "20", // 20 is shared.FailedJob.ID + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), + httpmock.JSONResponse(shared.FailedJob)) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), + httpmock.StringResponse("{}")) + }, + wantOut: "✓ Requested rerun of job 20 on run 1234\n", + }, { name: "prompt", tty: true, @@ -209,7 +252,7 @@ func TestRerun(t *testing.T) { httpmock.StatusStringResponse(403, "no")) }, wantErr: true, - errOut: "run 3 cannot be rerun; its workflow file may be broken.", + errOut: "run 3 cannot be rerun; its workflow file may be broken", }, } From 3645975da763411b0c0d9b7482be5bed774b6509 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:00:29 -0700 Subject: [PATCH 4/7] Prefer IsStdoutTTY when that's all we need --- pkg/cmd/run/rerun/rerun.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 21a3c31d1..e382fd134 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -124,7 +124,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n", cs.SuccessIcon(), cs.Cyanf("%d", selectedJob.ID), @@ -142,7 +142,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + if opts.IO.IsStdoutTTY() { onlyFailedMsg := "" if opts.OnlyFailed { onlyFailedMsg = "(failed jobs) " From 24ec53365bdf505116485189ec432bcc19adfca0 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:01:19 -0700 Subject: [PATCH 5/7] Return error if both jobID and runID are specified --- pkg/cmd/run/rerun/rerun.go | 6 +----- pkg/cmd/run/rerun/rerun_test.go | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index e382fd134..0303060b4 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -50,11 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - opts.RunID = "" - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) - } + return cmdutil.FlagErrorf("specify only one of or ") } if runF != nil { diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 094425290..b7bfc1d62 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -76,12 +76,10 @@ func TestNewCmdRerun(t *testing.T) { }, }, { - name: "with args job and runID ignores runID", - tty: true, - cli: "1234 --job 5678", - wants: RerunOptions{ - JobID: "5678", - }, + name: "with args jobID and runID fails", + tty: true, + cli: "1234 --job 5678", + wantsErr: true, }, { name: "with arg job with no ID fails", From e6b09b45deb10c3d465b598e5ff91b4d3b22f2a4 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 12:29:49 -0700 Subject: [PATCH 6/7] Fix up error and help language --- pkg/cmd/run/rerun/rerun.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 0303060b4..45f231f90 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -41,7 +41,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm if len(args) == 0 && opts.JobID == "" { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("run or job ID required when not running interactively") + return cmdutil.FlagErrorf("`` or `--job` required when not running interactively") } else { opts.Prompt = true } @@ -50,7 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - return cmdutil.FlagErrorf("specify only one of or ") + return cmdutil.FlagErrorf("specify only one of `` or `--job`") } if runF != nil { @@ -60,7 +60,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs, including dependencies") cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") return cmd @@ -107,7 +107,7 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to get runs: %w", err) } if len(runs) == 0 { - return errors.New("no recent runs have failed; please specify a specific run ID") + return errors.New("no recent runs have failed; please specify a specific ``") } runID, err = shared.PromptForRun(cs, runs) if err != nil { From a20e8b7eec4005f3cf3e29f56ded121e90f675c6 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Tue, 15 Mar 2022 09:50:20 -0700 Subject: [PATCH 7/7] Fix test I missed updating --- pkg/cmd/run/rerun/rerun_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index b7bfc1d62..0f7c80997 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -233,7 +233,7 @@ func TestRerun(t *testing.T) { }})) }, wantErr: true, - errOut: "no recent runs have failed; please specify a specific run ID", + errOut: "no recent runs have failed; please specify a specific ``", }, { name: "unrerunnable",