Implement --job for rerunning a specific actions job

This commit is contained in:
Cameron Booth 2022-03-03 11:49:08 -08:00
parent c38ca830be
commit bf5801e646
2 changed files with 129 additions and 28 deletions

View file

@ -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
}

View file

@ -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",
},
}