Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"retry": "Retry",
"rerun": "Re-run",
"rerun_all": "Re-run all jobs",
"rerun_failed": "Re-run failed jobs",
"save": "Save",
"add": "Add",
"add_all": "Add All",
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ func Routes() *web.Router {
m.Get("", repo.GetWorkflowRun)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
m.Get("/jobs", repo.ListWorkflowRunJobs)
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
m.Get("/artifacts", repo.GetArtifactsOfRun)
Expand Down
53 changes: 53 additions & 0 deletions routers/api/v1/repo/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,59 @@ func RerunWorkflowRun(ctx *context.APIContext) {
ctx.JSON(http.StatusCreated, convertedRun)
}

// RerunFailedWorkflowRun Reruns all failed jobs in a workflow run.
func RerunFailedWorkflowRun(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs repository rerunFailedWorkflowRun
// ---
// summary: Reruns all failed jobs in a workflow run
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/WorkflowRun"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"

run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}

if err := actions_service.RerunFailedWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}

convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertedRun)
}

// RerunWorkflowJob Reruns a specific workflow job in a run.
func RerunWorkflowJob(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
Expand Down
58 changes: 47 additions & 11 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type ViewResponse struct {
CanCancel bool `json:"canCancel"`
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
CanRerun bool `json:"canRerun"`
CanRerunFailed bool `json:"canRerunFailed"`
CanDeleteArtifact bool `json:"canDeleteArtifact"`
Done bool `json:"done"`
WorkflowID string `json:"workflowID"`
Expand Down Expand Up @@ -238,6 +239,14 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
if resp.State.Run.CanRerun {
for _, job := range jobs {
if job.Status == actions_model.StatusFailure {
resp.State.Run.CanRerunFailed = true
break
}
}
}
resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.WorkflowID = run.WorkflowID
resp.State.Run.WorkflowLink = run.WorkflowLink()
Expand Down Expand Up @@ -398,6 +407,22 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
return viewJobs, logs, nil
}

// checkRunRerunAllowed checks whether a rerun is permitted for the given run,
// writing the appropriate JSON error to ctx and returning false when it is not.
func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.ActionRun) bool {
if !run.Status.IsDone() {
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
return false
}
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(run.WorkflowID) {
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
return false
}
return true
}

// Rerun will rerun jobs in the given run
// If jobIDStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
Expand All @@ -408,17 +433,7 @@ func Rerun(ctx *context_module.Context) {
return
}

// rerun is not allowed if the run is not done
if !run.Status.IsDone() {
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
return
}

// can not rerun job when workflow is disabled
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(run.WorkflowID) {
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
if !checkRunRerunAllowed(ctx, run) {
return
}

Expand All @@ -435,6 +450,27 @@ func Rerun(ctx *context_module.Context) {
ctx.JSONOK()
}

// RerunFailed reruns all failed jobs in the given run
func RerunFailed(ctx *context_module.Context) {
runID := getRunID(ctx)

run, jobs, _ := getRunJobsAndCurrentJob(ctx, runID)
if ctx.Written() {
return
}

if !checkRunRerunAllowed(ctx, run) {
return
}

if err := actions_service.RerunFailedWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
ctx.ServerError("RerunFailedWorkflowRunJobs", err)
return
}

ctx.JSONOK()
}

func Logs(ctx *context_module.Context) {
runID := getRunID(ctx)
jobID := ctx.PathParamInt64("job")
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Post("/rerun-failed", reqRepoActionsWriter, actions.RerunFailed)
})
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
Expand Down
92 changes: 78 additions & 14 deletions services/actions/rerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,27 @@ import (
"xorm.io/builder"
)

// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
// GetFailedRerunJobs returns all failed jobs and their downstream dependent jobs that need to be rerun
func GetFailedRerunJobs(allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
rerunJobIDSet := make(container.Set[int64])
var jobsToRerun []*actions_model.ActionRunJob

for _, job := range allJobs {
if job.Status == actions_model.StatusFailure {
for _, j := range GetAllRerunJobs(job, allJobs) {
if !rerunJobIDSet.Contains(j.ID) {
rerunJobIDSet.Add(j.ID)
jobsToRerun = append(jobsToRerun, j)
}
}
}
}

return jobsToRerun
}

// GetAllRerunJobs returns the target job and all jobs that transitively depend on it.
// Downstream jobs are included regardless of their current status.
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
rerunJobs := []*actions_model.ActionRunJob{job}
rerunJobsIDSet := make(container.Set[string])
Expand Down Expand Up @@ -49,20 +69,20 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A
return rerunJobs
}

// RerunWorkflowRunJobs reruns all done jobs of a workflow run,
// or reruns a selected job and all of its downstream jobs when targetJob is specified.
func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob, targetJob *actions_model.ActionRunJob) error {
// Rerun is not allowed if the run is not done.
// prepareRunRerun validates the run, resets its state, handles concurrency, persists the
// updated run, and fires a status-update notification.
// It returns isRunBlocked (true when the run itself is held by a concurrency group).
func prepareRunRerun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) (isRunBlocked bool, err error) {
if !run.Status.IsDone() {
return util.NewInvalidArgumentErrorf("this workflow run is not done")
return false, util.NewInvalidArgumentErrorf("this workflow run is not done")
}

cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)

// Rerun is not allowed when workflow is disabled.
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(run.WorkflowID) {
return util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID)
return false, util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID)
}

// Reset run's timestamps and status.
Expand All @@ -73,31 +93,31 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run

vars, err := actions_model.GetVariablesOfRun(ctx, run)
if err != nil {
return fmt.Errorf("get run %d variables: %w", run.ID, err)
return false, fmt.Errorf("get run %d variables: %w", run.ID, err)
}

if run.RawConcurrency != "" {
var rawConcurrency model.RawConcurrency
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
return fmt.Errorf("unmarshal raw concurrency: %w", err)
return false, fmt.Errorf("unmarshal raw concurrency: %w", err)
}

if err := EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil); err != nil {
return err
return false, err
}

run.Status, err = PrepareToStartRunWithConcurrency(ctx, run)
if err != nil {
return err
return false, err
}
}

if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
return err
return false, err
}

if err := run.LoadAttributes(ctx); err != nil {
return err
return false, err
}

for _, job := range jobs {
Expand All @@ -106,7 +126,16 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run

notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)

isRunBlocked := run.Status == actions_model.StatusBlocked
return run.Status == actions_model.StatusBlocked, nil
}

// RerunWorkflowRunJobs reruns all done jobs of a workflow run,
// or reruns a selected job and all of its downstream jobs when targetJob is specified.
func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob, targetJob *actions_model.ActionRunJob) error {
isRunBlocked, err := prepareRunRerun(ctx, repo, run, jobs)
if err != nil {
return err
}

if targetJob == nil {
for _, job := range jobs {
Expand All @@ -131,6 +160,41 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run
return nil
}

// RerunFailedWorkflowRunJobs reruns all failed jobs of a workflow run and their downstream dependencies.
func RerunFailedWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) error {
isRunBlocked, err := prepareRunRerun(ctx, repo, run, jobs)
if err != nil {
return err
}

jobsToRerun := GetFailedRerunJobs(jobs)
if len(jobsToRerun) == 0 {
return nil
}

rerunJobByJobID := make(container.Set[string])
for _, j := range jobsToRerun {
rerunJobByJobID.Add(j.JobID)
}

for _, job := range jobsToRerun {
shouldBlockJob := isRunBlocked
if !shouldBlockJob {
for _, need := range job.Needs {
if rerunJobByJobID.Contains(need) {
shouldBlockJob = true
break
}
}
}
if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
return err
}
}

return nil
}

func rerunWorkflowJob(ctx context.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status
if !status.IsDone() {
Expand Down
Loading
Loading