Skip to content

Commit bf5801e

Browse files
committed
Implement --job for rerunning a specific actions job
1 parent c38ca83 commit bf5801e

File tree

2 files changed

+126
-25
lines changed

2 files changed

+126
-25
lines changed

pkg/cmd/run/rerun/rerun.go

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type RerunOptions struct {
2020

2121
RunID string
2222
OnlyFailed bool
23+
JobID string
2324

2425
Prompt bool
2526
}
@@ -38,12 +39,22 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
3839
// support `-R, --repo` override
3940
opts.BaseRepo = f.BaseRepo
4041

41-
if len(args) > 0 {
42+
if len(args) == 0 && opts.JobID == "" {
43+
if !opts.IO.CanPrompt() {
44+
return cmdutil.FlagErrorf("run or job ID required when not running interactively")
45+
} else {
46+
opts.Prompt = true
47+
}
48+
} else if len(args) > 0 {
4249
opts.RunID = args[0]
43-
} else if !opts.IO.CanPrompt() {
44-
return cmdutil.FlagErrorf("run ID required when not running interactively")
45-
} else {
46-
opts.Prompt = true
50+
}
51+
52+
if opts.RunID != "" && opts.JobID != "" {
53+
opts.RunID = ""
54+
if opts.IO.CanPrompt() {
55+
cs := opts.IO.ColorScheme()
56+
fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon())
57+
}
4758
}
4859

4960
if runF != nil {
@@ -54,6 +65,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
5465
}
5566

5667
cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs")
68+
cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies")
5769

5870
return cmd
5971
}
@@ -70,10 +82,23 @@ func runRerun(opts *RerunOptions) error {
7082
return fmt.Errorf("failed to determine base repo: %w", err)
7183
}
7284

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

75101
if opts.Prompt {
76-
cs := opts.IO.ColorScheme()
77102
runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool {
78103
if run.Status != shared.Completed {
79104
return false
@@ -94,40 +119,73 @@ func runRerun(opts *RerunOptions) error {
94119
}
95120
}
96121

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

157+
return nil
158+
}
159+
160+
func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed bool) error {
104161
runVerb := "rerun"
105-
if opts.OnlyFailed {
162+
if onlyFailed {
106163
runVerb = "rerun-failed-jobs"
107164
}
108165

109166
path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb)
110167

111-
err = client.REST(repo.RepoHost(), "POST", path, nil, nil)
168+
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
112169
if err != nil {
113170
var httpError api.HTTPError
114171
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
115-
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken.", run.ID)
172+
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID)
116173
}
117174
return fmt.Errorf("failed to rerun: %w", err)
118175
}
176+
return nil
177+
}
178+
179+
func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job) error {
180+
path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID)
119181

120-
if opts.IO.CanPrompt() {
121-
cs := opts.IO.ColorScheme()
122-
onlyFailedMsg := ""
123-
if opts.OnlyFailed {
124-
onlyFailedMsg = "(failed jobs) "
182+
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
183+
if err != nil {
184+
var httpError api.HTTPError
185+
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
186+
return fmt.Errorf("job %d cannot be rerun", job.ID)
125187
}
126-
fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n",
127-
cs.SuccessIcon(),
128-
onlyFailedMsg,
129-
cs.Cyanf("%d", run.ID))
188+
return fmt.Errorf("failed to rerun: %w", err)
130189
}
131-
132190
return nil
133191
}

pkg/cmd/run/rerun/rerun_test.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,33 @@ func TestNewCmdRerun(t *testing.T) {
6767
OnlyFailed: true,
6868
},
6969
},
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 job and runID ignores runID",
80+
tty: true,
81+
cli: "1234 --job 5678",
82+
wants: RerunOptions{
83+
JobID: "5678",
84+
},
85+
},
86+
{
87+
name: "with arg job with no ID fails",
88+
tty: true,
89+
cli: "--job",
90+
wantsErr: true,
91+
},
92+
{
93+
name: "with arg job with no ID no tty fails",
94+
cli: "--job",
95+
wantsErr: true,
96+
},
7097
}
7198

7299
for _, tt := range tests {
@@ -151,6 +178,22 @@ func TestRerun(t *testing.T) {
151178
},
152179
wantOut: "✓ Requested rerun (failed jobs) of run 1234\n",
153180
},
181+
{
182+
name: "arg including a specific job",
183+
tty: true,
184+
opts: &RerunOptions{
185+
JobID: "20", // 20 is shared.FailedJob.ID
186+
},
187+
httpStubs: func(reg *httpmock.Registry) {
188+
reg.Register(
189+
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
190+
httpmock.JSONResponse(shared.FailedJob))
191+
reg.Register(
192+
httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"),
193+
httpmock.StringResponse("{}"))
194+
},
195+
wantOut: "✓ Requested rerun of job 20 on run 1234\n",
196+
},
154197
{
155198
name: "prompt",
156199
tty: true,
@@ -209,7 +252,7 @@ func TestRerun(t *testing.T) {
209252
httpmock.StatusStringResponse(403, "no"))
210253
},
211254
wantErr: true,
212-
errOut: "run 3 cannot be rerun; its workflow file may be broken.",
255+
errOut: "run 3 cannot be rerun; its workflow file may be broken",
213256
},
214257
}
215258

0 commit comments

Comments
 (0)