github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/run/rerun/rerun.go (about) 1 package rerun 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 11 "github.com/ungtb10d/cli/v2/api" 12 "github.com/ungtb10d/cli/v2/internal/ghrepo" 13 "github.com/ungtb10d/cli/v2/pkg/cmd/run/shared" 14 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 15 "github.com/ungtb10d/cli/v2/pkg/iostreams" 16 "github.com/spf13/cobra" 17 ) 18 19 type RerunOptions struct { 20 HttpClient func() (*http.Client, error) 21 IO *iostreams.IOStreams 22 BaseRepo func() (ghrepo.Interface, error) 23 24 RunID string 25 OnlyFailed bool 26 JobID string 27 Debug bool 28 29 Prompt bool 30 } 31 32 func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Command { 33 opts := &RerunOptions{ 34 IO: f.IOStreams, 35 HttpClient: f.HttpClient, 36 } 37 38 cmd := &cobra.Command{ 39 Use: "rerun [<run-id>]", 40 Short: "Rerun a failed run", 41 Args: cobra.MaximumNArgs(1), 42 RunE: func(cmd *cobra.Command, args []string) error { 43 // support `-R, --repo` override 44 opts.BaseRepo = f.BaseRepo 45 46 if len(args) == 0 && opts.JobID == "" { 47 if !opts.IO.CanPrompt() { 48 return cmdutil.FlagErrorf("`<run-id>` or `--job` required when not running interactively") 49 } else { 50 opts.Prompt = true 51 } 52 } else if len(args) > 0 { 53 opts.RunID = args[0] 54 } 55 56 if opts.RunID != "" && opts.JobID != "" { 57 return cmdutil.FlagErrorf("specify only one of `<run-id>` or `--job`") 58 } 59 60 if runF != nil { 61 return runF(opts) 62 } 63 return runRerun(opts) 64 }, 65 } 66 67 cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs, including dependencies") 68 cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") 69 cmd.Flags().BoolVarP(&opts.Debug, "debug", "d", false, "Rerun with debug logging") 70 71 return cmd 72 } 73 74 func runRerun(opts *RerunOptions) error { 75 c, err := opts.HttpClient() 76 if err != nil { 77 return fmt.Errorf("failed to create http client: %w", err) 78 } 79 client := api.NewClientFromHTTP(c) 80 81 repo, err := opts.BaseRepo() 82 if err != nil { 83 return fmt.Errorf("failed to determine base repo: %w", err) 84 } 85 86 cs := opts.IO.ColorScheme() 87 88 runID := opts.RunID 89 jobID := opts.JobID 90 var selectedJob *shared.Job 91 92 if jobID != "" { 93 opts.IO.StartProgressIndicator() 94 selectedJob, err = shared.GetJob(client, repo, jobID) 95 opts.IO.StopProgressIndicator() 96 if err != nil { 97 return fmt.Errorf("failed to get job: %w", err) 98 } 99 runID = fmt.Sprintf("%d", selectedJob.RunID) 100 } 101 102 if opts.Prompt { 103 runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool { 104 if run.Status != shared.Completed { 105 return false 106 } 107 // TODO StartupFailure indicates a bad yaml file; such runs can never be 108 // rerun. But hiding them from the prompt might confuse people? 109 return run.Conclusion != shared.Success && run.Conclusion != shared.StartupFailure 110 }) 111 if err != nil { 112 return fmt.Errorf("failed to get runs: %w", err) 113 } 114 if len(runs) == 0 { 115 return errors.New("no recent runs have failed; please specify a specific `<run-id>`") 116 } 117 runID, err = shared.PromptForRun(cs, runs) 118 if err != nil { 119 return err 120 } 121 } 122 123 debugMsg := "" 124 if opts.Debug { 125 debugMsg = " with debug logging enabled" 126 } 127 128 if opts.JobID != "" { 129 err = rerunJob(client, repo, selectedJob, opts.Debug) 130 if err != nil { 131 return err 132 } 133 if opts.IO.IsStdoutTTY() { 134 fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s%s\n", 135 cs.SuccessIcon(), 136 cs.Cyanf("%d", selectedJob.ID), 137 cs.Cyanf("%d", selectedJob.RunID), 138 debugMsg) 139 } 140 } else { 141 opts.IO.StartProgressIndicator() 142 run, err := shared.GetRun(client, repo, runID) 143 opts.IO.StopProgressIndicator() 144 if err != nil { 145 return fmt.Errorf("failed to get run: %w", err) 146 } 147 148 err = rerunRun(client, repo, run, opts.OnlyFailed, opts.Debug) 149 if err != nil { 150 return err 151 } 152 if opts.IO.IsStdoutTTY() { 153 onlyFailedMsg := "" 154 if opts.OnlyFailed { 155 onlyFailedMsg = "(failed jobs) " 156 } 157 fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s%s\n", 158 cs.SuccessIcon(), 159 onlyFailedMsg, 160 cs.Cyanf("%d", run.ID), 161 debugMsg) 162 } 163 } 164 165 return nil 166 } 167 168 func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed, debug bool) error { 169 runVerb := "rerun" 170 if onlyFailed { 171 runVerb = "rerun-failed-jobs" 172 } 173 174 body, err := requestBody(debug) 175 if err != nil { 176 return fmt.Errorf("failed to create rerun body: %w", err) 177 } 178 179 path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb) 180 181 err = client.REST(repo.RepoHost(), "POST", path, body, nil) 182 if err != nil { 183 var httpError api.HTTPError 184 if errors.As(err, &httpError) && httpError.StatusCode == 403 { 185 return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID) 186 } 187 return fmt.Errorf("failed to rerun: %w", err) 188 } 189 return nil 190 } 191 192 func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job, debug bool) error { 193 body, err := requestBody(debug) 194 if err != nil { 195 return fmt.Errorf("failed to create rerun body: %w", err) 196 } 197 198 path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID) 199 200 err = client.REST(repo.RepoHost(), "POST", path, body, nil) 201 if err != nil { 202 var httpError api.HTTPError 203 if errors.As(err, &httpError) && httpError.StatusCode == 403 { 204 return fmt.Errorf("job %d cannot be rerun", job.ID) 205 } 206 return fmt.Errorf("failed to rerun: %w", err) 207 } 208 return nil 209 } 210 211 type RerunPayload struct { 212 Debug bool `json:"enable_debug_logging"` 213 } 214 215 func requestBody(debug bool) (io.Reader, error) { 216 if !debug { 217 return nil, nil 218 } 219 params := &RerunPayload{ 220 debug, 221 } 222 223 body := &bytes.Buffer{} 224 enc := json.NewEncoder(body) 225 if err := enc.Encode(params); err != nil { 226 return nil, err 227 } 228 return body, nil 229 }