Skip to content

Commit 1c26019

Browse files
author
Nate Smith
authored
Merge pull request cli#5275 from cdb/cdb/rerun-failed-jobs
Support "all failed jobs" and individual job re-run for a github actions workflow run
2 parents 43e4e95 + a20e8b7 commit 1c26019

File tree

4 files changed

+179
-37
lines changed

4 files changed

+179
-37
lines changed

pkg/cmd/run/rerun/rerun.go

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ type RerunOptions struct {
1818
IO *iostreams.IOStreams
1919
BaseRepo func() (ghrepo.Interface, error)
2020

21-
RunID string
21+
RunID string
22+
OnlyFailed bool
23+
JobID string
2224

2325
Prompt bool
2426
}
@@ -37,12 +39,18 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
3739
// support `-R, --repo` override
3840
opts.BaseRepo = f.BaseRepo
3941

40-
if len(args) > 0 {
42+
if len(args) == 0 && opts.JobID == "" {
43+
if !opts.IO.CanPrompt() {
44+
return cmdutil.FlagErrorf("`<run-id>` or `--job` required when not running interactively")
45+
} else {
46+
opts.Prompt = true
47+
}
48+
} else if len(args) > 0 {
4149
opts.RunID = args[0]
42-
} else if !opts.IO.CanPrompt() {
43-
return cmdutil.FlagErrorf("run ID required when not running interactively")
44-
} else {
45-
opts.Prompt = true
50+
}
51+
52+
if opts.RunID != "" && opts.JobID != "" {
53+
return cmdutil.FlagErrorf("specify only one of `<run-id>` or `--job`")
4654
}
4755

4856
if runF != nil {
@@ -52,6 +60,9 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
5260
},
5361
}
5462

63+
cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs, including dependencies")
64+
cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies")
65+
5566
return cmd
5667
}
5768

@@ -67,10 +78,23 @@ func runRerun(opts *RerunOptions) error {
6778
return fmt.Errorf("failed to determine base repo: %w", err)
6879
}
6980

81+
cs := opts.IO.ColorScheme()
82+
7083
runID := opts.RunID
84+
jobID := opts.JobID
85+
var selectedJob *shared.Job
86+
87+
if jobID != "" {
88+
opts.IO.StartProgressIndicator()
89+
selectedJob, err = shared.GetJob(client, repo, jobID)
90+
opts.IO.StopProgressIndicator()
91+
if err != nil {
92+
return fmt.Errorf("failed to get job: %w", err)
93+
}
94+
runID = fmt.Sprintf("%d", selectedJob.RunID)
95+
}
7196

7297
if opts.Prompt {
73-
cs := opts.IO.ColorScheme()
7498
runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool {
7599
if run.Status != shared.Completed {
76100
return false
@@ -83,38 +107,81 @@ func runRerun(opts *RerunOptions) error {
83107
return fmt.Errorf("failed to get runs: %w", err)
84108
}
85109
if len(runs) == 0 {
86-
return errors.New("no recent runs have failed; please specify a specific run ID")
110+
return errors.New("no recent runs have failed; please specify a specific `<run-id>`")
87111
}
88112
runID, err = shared.PromptForRun(cs, runs)
89113
if err != nil {
90114
return err
91115
}
92116
}
93117

94-
opts.IO.StartProgressIndicator()
95-
run, err := shared.GetRun(client, repo, runID)
96-
opts.IO.StopProgressIndicator()
97-
if err != nil {
98-
return fmt.Errorf("failed to get run: %w", err)
118+
if opts.JobID != "" {
119+
err = rerunJob(client, repo, selectedJob)
120+
if err != nil {
121+
return err
122+
}
123+
if opts.IO.IsStdoutTTY() {
124+
fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n",
125+
cs.SuccessIcon(),
126+
cs.Cyanf("%d", selectedJob.ID),
127+
cs.Cyanf("%d", selectedJob.RunID))
128+
}
129+
} else {
130+
opts.IO.StartProgressIndicator()
131+
run, err := shared.GetRun(client, repo, runID)
132+
opts.IO.StopProgressIndicator()
133+
if err != nil {
134+
return fmt.Errorf("failed to get run: %w", err)
135+
}
136+
137+
err = rerunRun(client, repo, run, opts.OnlyFailed)
138+
if err != nil {
139+
return err
140+
}
141+
if opts.IO.IsStdoutTTY() {
142+
onlyFailedMsg := ""
143+
if opts.OnlyFailed {
144+
onlyFailedMsg = "(failed jobs) "
145+
}
146+
fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n",
147+
cs.SuccessIcon(),
148+
onlyFailedMsg,
149+
cs.Cyanf("%d", run.ID))
150+
}
99151
}
100152

101-
path := fmt.Sprintf("repos/%s/actions/runs/%d/rerun", ghrepo.FullName(repo), run.ID)
153+
return nil
154+
}
102155

103-
err = client.REST(repo.RepoHost(), "POST", path, nil, nil)
156+
func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed bool) error {
157+
runVerb := "rerun"
158+
if onlyFailed {
159+
runVerb = "rerun-failed-jobs"
160+
}
161+
162+
path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb)
163+
164+
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
104165
if err != nil {
105166
var httpError api.HTTPError
106167
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
107-
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken.", run.ID)
168+
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID)
108169
}
109170
return fmt.Errorf("failed to rerun: %w", err)
110171
}
172+
return nil
173+
}
111174

112-
if opts.IO.CanPrompt() {
113-
cs := opts.IO.ColorScheme()
114-
fmt.Fprintf(opts.IO.Out, "%s Requested rerun of run %s\n",
115-
cs.SuccessIcon(),
116-
cs.Cyanf("%d", run.ID))
117-
}
175+
func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job) error {
176+
path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID)
118177

178+
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
179+
if err != nil {
180+
var httpError api.HTTPError
181+
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
182+
return fmt.Errorf("job %d cannot be rerun", job.ID)
183+
}
184+
return fmt.Errorf("failed to rerun: %w", err)
185+
}
119186
return nil
120187
}

pkg/cmd/run/rerun/rerun_test.go

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,48 @@ func TestNewCmdRerun(t *testing.T) {
5050
RunID: "1234",
5151
},
5252
},
53+
{
54+
name: "failed arg nontty",
55+
cli: "4321 --failed",
56+
wants: RerunOptions{
57+
RunID: "4321",
58+
OnlyFailed: true,
59+
},
60+
},
61+
{
62+
name: "failed arg",
63+
tty: true,
64+
cli: "--failed",
65+
wants: RerunOptions{
66+
Prompt: true,
67+
OnlyFailed: true,
68+
},
69+
},
70+
{
71+
name: "with arg job",
72+
tty: true,
73+
cli: "--job 1234",
74+
wants: RerunOptions{
75+
JobID: "1234",
76+
},
77+
},
78+
{
79+
name: "with args jobID and runID fails",
80+
tty: true,
81+
cli: "1234 --job 5678",
82+
wantsErr: true,
83+
},
84+
{
85+
name: "with arg job with no ID fails",
86+
tty: true,
87+
cli: "--job",
88+
wantsErr: true,
89+
},
90+
{
91+
name: "with arg job with no ID no tty fails",
92+
cli: "--job",
93+
wantsErr: true,
94+
},
5395
}
5496

5597
for _, tt := range tests {
@@ -117,6 +159,39 @@ func TestRerun(t *testing.T) {
117159
},
118160
wantOut: "✓ Requested rerun of run 1234\n",
119161
},
162+
{
163+
name: "arg including onlyFailed",
164+
tty: true,
165+
opts: &RerunOptions{
166+
RunID: "1234",
167+
OnlyFailed: true,
168+
},
169+
httpStubs: func(reg *httpmock.Registry) {
170+
reg.Register(
171+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
172+
httpmock.JSONResponse(shared.FailedRun))
173+
reg.Register(
174+
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"),
175+
httpmock.StringResponse("{}"))
176+
},
177+
wantOut: "✓ Requested rerun (failed jobs) of run 1234\n",
178+
},
179+
{
180+
name: "arg including a specific job",
181+
tty: true,
182+
opts: &RerunOptions{
183+
JobID: "20", // 20 is shared.FailedJob.ID
184+
},
185+
httpStubs: func(reg *httpmock.Registry) {
186+
reg.Register(
187+
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
188+
httpmock.JSONResponse(shared.FailedJob))
189+
reg.Register(
190+
httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"),
191+
httpmock.StringResponse("{}"))
192+
},
193+
wantOut: "✓ Requested rerun of job 20 on run 1234\n",
194+
},
120195
{
121196
name: "prompt",
122197
tty: true,
@@ -158,7 +233,7 @@ func TestRerun(t *testing.T) {
158233
}}))
159234
},
160235
wantErr: true,
161-
errOut: "no recent runs have failed; please specify a specific run ID",
236+
errOut: "no recent runs have failed; please specify a specific `<run-id>`",
162237
},
163238
{
164239
name: "unrerunnable",
@@ -175,7 +250,7 @@ func TestRerun(t *testing.T) {
175250
httpmock.StatusStringResponse(403, "no"))
176251
},
177252
wantErr: true,
178-
errOut: "run 3 cannot be rerun; its workflow file may be broken.",
253+
errOut: "run 3 cannot be rerun; its workflow file may be broken",
179254
},
180255
}
181256

pkg/cmd/run/shared/shared.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,18 @@ func GetJobs(client *api.Client, repo ghrepo.Interface, run Run) ([]Job, error)
307307
return result.Jobs, nil
308308
}
309309

310+
func GetJob(client *api.Client, repo ghrepo.Interface, jobID string) (*Job, error) {
311+
path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID)
312+
313+
var result Job
314+
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
315+
if err != nil {
316+
return nil, err
317+
}
318+
319+
return &result, nil
320+
}
321+
310322
func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) {
311323
var selected int
312324
now := time.Now()

pkg/cmd/run/view/view.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func runView(opts *ViewOptions) error {
183183

184184
if jobID != "" {
185185
opts.IO.StartProgressIndicator()
186-
selectedJob, err = getJob(client, repo, jobID)
186+
selectedJob, err = shared.GetJob(client, repo, jobID)
187187
opts.IO.StopProgressIndicator()
188188
if err != nil {
189189
return fmt.Errorf("failed to get job: %w", err)
@@ -395,18 +395,6 @@ func runView(opts *ViewOptions) error {
395395
return nil
396396
}
397397

398-
func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) {
399-
path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID)
400-
401-
var result shared.Job
402-
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
403-
if err != nil {
404-
return nil, err
405-
}
406-
407-
return &result, nil
408-
}
409-
410398
func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
411399
req, err := http.NewRequest("GET", logURL, nil)
412400
if err != nil {

0 commit comments

Comments
 (0)