mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 07:48:48 +01:00
Feature: Add button to re-run failed jobs in Actions (#36924)
Fixes #35997 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -81,6 +81,7 @@
|
|||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"rerun": "Re-run",
|
"rerun": "Re-run",
|
||||||
"rerun_all": "Re-run all jobs",
|
"rerun_all": "Re-run all jobs",
|
||||||
|
"rerun_failed": "Re-run failed jobs",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"add_all": "Add All",
|
"add_all": "Add All",
|
||||||
|
|||||||
@@ -1259,6 +1259,7 @@ func Routes() *web.Router {
|
|||||||
m.Get("", repo.GetWorkflowRun)
|
m.Get("", repo.GetWorkflowRun)
|
||||||
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
|
||||||
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
|
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.Get("/jobs", repo.ListWorkflowRunJobs)
|
||||||
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
|
||||||
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
m.Get("/artifacts", repo.GetArtifactsOfRun)
|
||||||
|
|||||||
@@ -1255,7 +1255,7 @@ func RerunWorkflowRun(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, nil); err != nil {
|
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
|
||||||
handleWorkflowRerunError(ctx, err)
|
handleWorkflowRerunError(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1268,6 +1268,52 @@ func RerunWorkflowRun(ctx *context.APIContext) {
|
|||||||
ctx.JSON(http.StatusCreated, convertedRun)
|
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
|
||||||
|
// 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/empty"
|
||||||
|
// "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.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
|
||||||
|
handleWorkflowRerunError(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
// RerunWorkflowJob Reruns a specific workflow job in a run.
|
// RerunWorkflowJob Reruns a specific workflow job in a run.
|
||||||
func RerunWorkflowJob(ctx *context.APIContext) {
|
func RerunWorkflowJob(ctx *context.APIContext) {
|
||||||
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
|
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
|
||||||
@@ -1321,7 +1367,7 @@ func RerunWorkflowJob(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetJob := jobs[jobIdx]
|
targetJob := jobs[jobIdx]
|
||||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
|
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil {
|
||||||
handleWorkflowRerunError(ctx, err)
|
handleWorkflowRerunError(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
|||||||
resp.State.Run.CanCancel = runID == 10
|
resp.State.Run.CanCancel = runID == 10
|
||||||
resp.State.Run.CanApprove = runID == 20
|
resp.State.Run.CanApprove = runID == 20
|
||||||
resp.State.Run.CanRerun = runID == 30
|
resp.State.Run.CanRerun = runID == 30
|
||||||
|
resp.State.Run.CanRerunFailed = runID == 30
|
||||||
resp.State.Run.CanDeleteArtifact = true
|
resp.State.Run.CanDeleteArtifact = true
|
||||||
resp.State.Run.WorkflowID = "workflow-id"
|
resp.State.Run.WorkflowID = "workflow-id"
|
||||||
resp.State.Run.WorkflowLink = "./workflow-link"
|
resp.State.Run.WorkflowLink = "./workflow-link"
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type ViewResponse struct {
|
|||||||
CanCancel bool `json:"canCancel"`
|
CanCancel bool `json:"canCancel"`
|
||||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||||
CanRerun bool `json:"canRerun"`
|
CanRerun bool `json:"canRerun"`
|
||||||
|
CanRerunFailed bool `json:"canRerunFailed"`
|
||||||
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
WorkflowID string `json:"workflowID"`
|
WorkflowID string `json:"workflowID"`
|
||||||
@@ -238,6 +239,14 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
|
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.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||||
resp.State.Run.CanDeleteArtifact = 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 || job.Status == actions_model.StatusCancelled {
|
||||||
|
resp.State.Run.CanRerunFailed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
resp.State.Run.Done = run.Status.IsDone()
|
resp.State.Run.Done = run.Status.IsDone()
|
||||||
resp.State.Run.WorkflowID = run.WorkflowID
|
resp.State.Run.WorkflowID = run.WorkflowID
|
||||||
resp.State.Run.WorkflowLink = run.WorkflowLink()
|
resp.State.Run.WorkflowLink = run.WorkflowLink()
|
||||||
@@ -398,6 +407,22 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
|
|||||||
return viewJobs, logs, nil
|
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
|
// Rerun will rerun jobs in the given run
|
||||||
// If jobIDStr is a blank string, it means rerun all jobs
|
// If jobIDStr is a blank string, it means rerun all jobs
|
||||||
func Rerun(ctx *context_module.Context) {
|
func Rerun(ctx *context_module.Context) {
|
||||||
@@ -408,26 +433,39 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// rerun is not allowed if the run is not done
|
if !checkRunRerunAllowed(ctx, run) {
|
||||||
if !run.Status.IsDone() {
|
|
||||||
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// can not rerun job when workflow is disabled
|
var jobsToRerun []*actions_model.ActionRunJob
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetJob *actions_model.ActionRunJob // nil means rerun all jobs
|
|
||||||
if ctx.PathParam("job") != "" {
|
if ctx.PathParam("job") != "" {
|
||||||
targetJob = currentJob
|
jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs)
|
||||||
|
} else {
|
||||||
|
jobsToRerun = jobs
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
|
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil {
|
||||||
|
ctx.ServerError("RerunWorkflowRunJobs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
|
||||||
ctx.ServerError("RerunWorkflowRunJobs", err)
|
ctx.ServerError("RerunWorkflowRunJobs", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1529,6 +1529,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
||||||
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
||||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||||
|
m.Post("/rerun-failed", reqRepoActionsWriter, actions.RerunFailed)
|
||||||
})
|
})
|
||||||
m.Group("/workflows/{workflow_name}", func() {
|
m.Group("/workflows/{workflow_name}", func() {
|
||||||
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
|
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
|
||||||
|
|||||||
@@ -20,7 +20,27 @@ import (
|
|||||||
"xorm.io/builder"
|
"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 || job.Status == actions_model.StatusCancelled {
|
||||||
|
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 {
|
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
|
||||||
rerunJobs := []*actions_model.ActionRunJob{job}
|
rerunJobs := []*actions_model.ActionRunJob{job}
|
||||||
rerunJobsIDSet := make(container.Set[string])
|
rerunJobsIDSet := make(container.Set[string])
|
||||||
@@ -49,12 +69,12 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A
|
|||||||
return rerunJobs
|
return rerunJobs
|
||||||
}
|
}
|
||||||
|
|
||||||
// RerunWorkflowRunJobs reruns all done jobs of a workflow run,
|
// prepareRunRerun validates the run, resets its state, handles concurrency, persists the
|
||||||
// or reruns a selected job and all of its downstream jobs when targetJob is specified.
|
// updated run, and fires a status-update notification.
|
||||||
func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob, targetJob *actions_model.ActionRunJob) error {
|
// It returns isRunBlocked (true when the run itself is held by a concurrency group).
|
||||||
// Rerun is not allowed if the run is not done.
|
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() {
|
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)
|
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||||
@@ -62,7 +82,7 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run
|
|||||||
// Rerun is not allowed when workflow is disabled.
|
// Rerun is not allowed when workflow is disabled.
|
||||||
cfg := cfgUnit.ActionsConfig()
|
cfg := cfgUnit.ActionsConfig()
|
||||||
if cfg.IsWorkflowDisabled(run.WorkflowID) {
|
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.
|
// Reset run's timestamps and status.
|
||||||
@@ -73,31 +93,31 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run
|
|||||||
|
|
||||||
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
if err != nil {
|
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 != "" {
|
if run.RawConcurrency != "" {
|
||||||
var rawConcurrency model.RawConcurrency
|
var rawConcurrency model.RawConcurrency
|
||||||
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
|
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 {
|
if err := EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
run.Status, err = PrepareToStartRunWithConcurrency(ctx, run)
|
run.Status, err = PrepareToStartRunWithConcurrency(ctx, run)
|
||||||
if err != nil {
|
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 {
|
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 {
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
@@ -106,23 +126,38 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run
|
|||||||
|
|
||||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||||
|
|
||||||
isRunBlocked := run.Status == actions_model.StatusBlocked
|
return run.Status == actions_model.StatusBlocked, nil
|
||||||
|
}
|
||||||
|
|
||||||
if targetJob == nil {
|
// RerunWorkflowRunJobs reruns the given jobs of a workflow run.
|
||||||
for _, job := range jobs {
|
// jobsToRerun must include all jobs to be rerun (the target job and its transitively dependent jobs).
|
||||||
// If the job has needs, it should be blocked to wait for its dependencies.
|
// A job is blocked (waiting for dependencies) if the run itself is blocked or if any of its
|
||||||
shouldBlockJob := len(job.Needs) > 0 || isRunBlocked
|
// needs are also being rerun.
|
||||||
if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
|
func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobsToRerun []*actions_model.ActionRunJob) error {
|
||||||
return err
|
if len(jobsToRerun) == 0 {
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rerunJobs := GetAllRerunJobs(targetJob, jobs)
|
isRunBlocked, err := prepareRunRerun(ctx, repo, run, jobsToRerun)
|
||||||
for _, job := range rerunJobs {
|
if err != nil {
|
||||||
// Jobs other than the selected one should wait for dependencies.
|
return err
|
||||||
shouldBlockJob := job.JobID != targetJob.JobID || isRunBlocked
|
}
|
||||||
|
|
||||||
|
rerunJobIDs := make(container.Set[string])
|
||||||
|
for _, j := range jobsToRerun {
|
||||||
|
rerunJobIDs.Add(j.JobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range jobsToRerun {
|
||||||
|
shouldBlockJob := isRunBlocked
|
||||||
|
if !shouldBlockJob {
|
||||||
|
for _, need := range job.Needs {
|
||||||
|
if rerunJobIDs.Contains(need) {
|
||||||
|
shouldBlockJob = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
|
if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetAllRerunJobs(t *testing.T) {
|
func TestGetAllRerunJobs(t *testing.T) {
|
||||||
@@ -46,3 +49,97 @@ func TestGetAllRerunJobs(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
|
assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetFailedRerunJobs(t *testing.T) {
|
||||||
|
// IDs must be non-zero to distinguish jobs in the dedup set.
|
||||||
|
makeJob := func(id int64, jobID string, status actions_model.Status, needs ...string) *actions_model.ActionRunJob {
|
||||||
|
return &actions_model.ActionRunJob{ID: id, JobID: jobID, Status: status, Needs: needs}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no failed jobs returns empty", func(t *testing.T) {
|
||||||
|
jobs := []*actions_model.ActionRunJob{
|
||||||
|
makeJob(1, "job1", actions_model.StatusSuccess),
|
||||||
|
makeJob(2, "job2", actions_model.StatusSkipped, "job1"),
|
||||||
|
}
|
||||||
|
assert.Empty(t, GetFailedRerunJobs(jobs))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single failed job with no dependents", func(t *testing.T) {
|
||||||
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
||||||
|
job2 := makeJob(2, "job2", actions_model.StatusSuccess)
|
||||||
|
jobs := []*actions_model.ActionRunJob{job1, job2}
|
||||||
|
|
||||||
|
result := GetFailedRerunJobs(jobs)
|
||||||
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed job pulls in downstream dependents", func(t *testing.T) {
|
||||||
|
// job1 failed; job2 depends on job1 (skipped); job3 depends on job2 (skipped)
|
||||||
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
||||||
|
job2 := makeJob(2, "job2", actions_model.StatusSkipped, "job1")
|
||||||
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job2")
|
||||||
|
job4 := makeJob(4, "job4", actions_model.StatusSuccess) // unrelated, must not appear
|
||||||
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
|
||||||
|
|
||||||
|
result := GetFailedRerunJobs(jobs)
|
||||||
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple independent failed jobs each pull in their own dependents", func(t *testing.T) {
|
||||||
|
// job1 failed -> job3 depends on job1
|
||||||
|
// job2 failed -> job4 depends on job2
|
||||||
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
||||||
|
job2 := makeJob(2, "job2", actions_model.StatusFailure)
|
||||||
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1")
|
||||||
|
job4 := makeJob(4, "job4", actions_model.StatusSkipped, "job2")
|
||||||
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
|
||||||
|
|
||||||
|
result := GetFailedRerunJobs(jobs)
|
||||||
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3, job4}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shared downstream dependent is not duplicated", func(t *testing.T) {
|
||||||
|
// job1 and job2 both failed; job3 depends on both
|
||||||
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
||||||
|
job2 := makeJob(2, "job2", actions_model.StatusFailure)
|
||||||
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1", "job2")
|
||||||
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3}
|
||||||
|
|
||||||
|
result := GetFailedRerunJobs(jobs)
|
||||||
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result)
|
||||||
|
assert.Len(t, result, 3) // job3 must appear exactly once
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("successful downstream job of a failed job is still included", func(t *testing.T) {
|
||||||
|
// job1 failed; job2 succeeded but depends on job1 — downstream is always rerun
|
||||||
|
// regardless of its own status (GetAllRerunJobs includes all transitive dependents)
|
||||||
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
||||||
|
job2 := makeJob(2, "job2", actions_model.StatusSuccess, "job1")
|
||||||
|
jobs := []*actions_model.ActionRunJob{job1, job2}
|
||||||
|
|
||||||
|
result := GetFailedRerunJobs(jobs)
|
||||||
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRerunValidation(t *testing.T) {
|
||||||
|
runningRun := &actions_model.ActionRun{Status: actions_model.StatusRunning}
|
||||||
|
|
||||||
|
t.Run("RerunWorkflowRunJobs rejects a non-done run", func(t *testing.T) {
|
||||||
|
jobs := []*actions_model.ActionRunJob{
|
||||||
|
{ID: 1, JobID: "job1"},
|
||||||
|
}
|
||||||
|
err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, jobs)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, util.ErrInvalidArgument)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RerunWorkflowRunJobs rejects a non-done run when failed jobs exist", func(t *testing.T) {
|
||||||
|
jobs := []*actions_model.ActionRunJob{
|
||||||
|
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure},
|
||||||
|
}
|
||||||
|
err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, GetFailedRerunJobs(jobs))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, util.ErrInvalidArgument)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}"
|
data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}"
|
||||||
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
|
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
|
||||||
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
|
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
|
||||||
|
data-locale-rerun-failed="{{ctx.Locale.Tr "rerun_failed"}}"
|
||||||
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"
|
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"
|
||||||
data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}"
|
data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}"
|
||||||
data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}"
|
data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}"
|
||||||
|
|||||||
49
templates/swagger/v1_json.tmpl
generated
49
templates/swagger/v1_json.tmpl
generated
@@ -5578,6 +5578,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Reruns all failed jobs in a workflow run",
|
||||||
|
"operationId": "rerunFailedWorkflowRun",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "id of the run",
|
||||||
|
"name": "run",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/empty"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/actions/secrets": {
|
"/repos/{owner}/{repo}/actions/secrets": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export default defineComponent({
|
|||||||
canCancel: false,
|
canCancel: false,
|
||||||
canApprove: false,
|
canApprove: false,
|
||||||
canRerun: false,
|
canRerun: false,
|
||||||
|
canRerunFailed: false,
|
||||||
canDeleteArtifact: false,
|
canDeleteArtifact: false,
|
||||||
done: false,
|
done: false,
|
||||||
workflowID: '',
|
workflowID: '',
|
||||||
@@ -512,9 +513,24 @@ export default defineComponent({
|
|||||||
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
|
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
|
||||||
{{ locale.cancel }}
|
{{ locale.cancel }}
|
||||||
</button>
|
</button>
|
||||||
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
|
<template v-else-if="run.canRerun">
|
||||||
{{ locale.rerun_all }}
|
<div v-if="run.canRerunFailed" class="ui small compact buttons">
|
||||||
</button>
|
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun-failed`">
|
||||||
|
{{ locale.rerun_failed }}
|
||||||
|
</button>
|
||||||
|
<div class="ui basic small compact dropdown icon button">
|
||||||
|
<SvgIcon name="octicon-triangle-down" :size="14"/>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item link-action" :data-url="`${run.link}/rerun`">
|
||||||
|
{{ locale.rerun_all }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-else class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`">
|
||||||
|
{{ locale.rerun_all }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-commit-summary">
|
<div class="action-commit-summary">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function initRepositoryActionView() {
|
|||||||
cancel: el.getAttribute('data-locale-cancel'),
|
cancel: el.getAttribute('data-locale-cancel'),
|
||||||
rerun: el.getAttribute('data-locale-rerun'),
|
rerun: el.getAttribute('data-locale-rerun'),
|
||||||
rerun_all: el.getAttribute('data-locale-rerun-all'),
|
rerun_all: el.getAttribute('data-locale-rerun-all'),
|
||||||
|
rerun_failed: el.getAttribute('data-locale-rerun-failed'),
|
||||||
scheduled: el.getAttribute('data-locale-runs-scheduled'),
|
scheduled: el.getAttribute('data-locale-runs-scheduled'),
|
||||||
commit: el.getAttribute('data-locale-runs-commit'),
|
commit: el.getAttribute('data-locale-runs-commit'),
|
||||||
pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
|
pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
|
||||||
|
|||||||
Reference in New Issue
Block a user