Backport #29955 by @Zettat123 Fix #28761 Fix #27884 Fix #28093 ## Changes ### Rerun all jobs When rerun all jobs, status of the jobs with `needs` will be set to `blocked` instead of `waiting`. Therefore, these jobs will not run until the required jobs are completed. ### Rerun a single job When a single job is rerun, its dependents should also be rerun, just like GitHub does (https://github.com/go-gitea/gitea/issues/28761#issuecomment-2008620820). In this case, only the specified job will be set to `waiting`, its dependents will be set to `blocked` to wait the job. ### Show warning if every job has `needs` If every job in a workflow has `needs`, all jobs will be blocked and no job can be run. So I add a warning message. <img src="https://github.com/go-gitea/gitea/assets/15528715/88f43511-2360-465d-be96-ee92b57ff67b" width="480px" /> --------- Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>tags/v1.21.9
runs.pushed_by = pushed by | runs.pushed_by = pushed by | ||||
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s | runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s | ||||
runs.no_matching_online_runner_helper = No matching online runner with label: %s | runs.no_matching_online_runner_helper = No matching online runner with label: %s | ||||
runs.no_job_without_needs = The workflow must contain at least one job without dependencies. | |||||
runs.actor = Actor | runs.actor = Actor | ||||
runs.status = Status | runs.status = Status | ||||
runs.actors_no_select = All actors | runs.actors_no_select = All actors |
workflows = append(workflows, workflow) | workflows = append(workflows, workflow) | ||||
continue | continue | ||||
} | } | ||||
// Check whether have matching runner | |||||
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. | |||||
hasJobWithoutNeeds := false | |||||
// Check whether have matching runner and a job without "needs" | |||||
for _, j := range wf.Jobs { | for _, j := range wf.Jobs { | ||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 { | |||||
hasJobWithoutNeeds = true | |||||
} | |||||
runsOnList := j.RunsOn() | runsOnList := j.RunsOn() | ||||
for _, ro := range runsOnList { | for _, ro := range runsOnList { | ||||
if strings.Contains(ro, "${{") { | if strings.Contains(ro, "${{") { | ||||
break | break | ||||
} | } | ||||
} | } | ||||
if !hasJobWithoutNeeds { | |||||
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_job_without_needs") | |||||
} | |||||
workflows = append(workflows, workflow) | workflows = append(workflows, workflow) | ||||
} | } | ||||
} | } |
return | return | ||||
} | } | ||||
if jobIndexStr != "" { | |||||
jobs = []*actions_model.ActionRunJob{job} | |||||
if jobIndexStr == "" { // rerun all jobs | |||||
for _, j := range jobs { | |||||
// if the job has needs, it should be set to "blocked" status to wait for other jobs | |||||
shouldBlock := len(j.Needs) > 0 | |||||
if err := rerunJob(ctx, j, shouldBlock); err != nil { | |||||
ctx.Error(http.StatusInternalServerError, err.Error()) | |||||
return | |||||
} | |||||
} | |||||
ctx.JSON(http.StatusOK, struct{}{}) | |||||
return | |||||
} | } | ||||
for _, j := range jobs { | |||||
if err := rerunJob(ctx, j); err != nil { | |||||
rerunJobs := actions_service.GetAllRerunJobs(job, jobs) | |||||
for _, j := range rerunJobs { | |||||
// jobs other than the specified one should be set to "blocked" status | |||||
shouldBlock := j.JobID != job.JobID | |||||
if err := rerunJob(ctx, j, shouldBlock); err != nil { | |||||
ctx.Error(http.StatusInternalServerError, err.Error()) | ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
return | return | ||||
} | } | ||||
ctx.JSON(http.StatusOK, struct{}{}) | ctx.JSON(http.StatusOK, struct{}{}) | ||||
} | } | ||||
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error { | |||||
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error { | |||||
status := job.Status | status := job.Status | ||||
if !status.IsDone() { | if !status.IsDone() { | ||||
return nil | return nil | ||||
job.TaskID = 0 | job.TaskID = 0 | ||||
job.Status = actions_model.StatusWaiting | job.Status = actions_model.StatusWaiting | ||||
if shouldBlock { | |||||
job.Status = actions_model.StatusBlocked | |||||
} | |||||
job.Started = 0 | job.Started = 0 | ||||
job.Stopped = 0 | job.Stopped = 0 | ||||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package actions | |||||
import ( | |||||
actions_model "code.gitea.io/gitea/models/actions" | |||||
"code.gitea.io/gitea/modules/container" | |||||
) | |||||
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun | |||||
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { | |||||
rerunJobs := []*actions_model.ActionRunJob{job} | |||||
rerunJobsIDSet := make(container.Set[string]) | |||||
rerunJobsIDSet.Add(job.JobID) | |||||
for { | |||||
found := false | |||||
for _, j := range allJobs { | |||||
if rerunJobsIDSet.Contains(j.JobID) { | |||||
continue | |||||
} | |||||
for _, need := range j.Needs { | |||||
if rerunJobsIDSet.Contains(need) { | |||||
found = true | |||||
rerunJobs = append(rerunJobs, j) | |||||
rerunJobsIDSet.Add(j.JobID) | |||||
break | |||||
} | |||||
} | |||||
} | |||||
if !found { | |||||
break | |||||
} | |||||
} | |||||
return rerunJobs | |||||
} |
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package actions | |||||
import ( | |||||
"testing" | |||||
actions_model "code.gitea.io/gitea/models/actions" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestGetAllRerunJobs(t *testing.T) { | |||||
job1 := &actions_model.ActionRunJob{JobID: "job1"} | |||||
job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}} | |||||
job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}} | |||||
job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}} | |||||
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} | |||||
testCases := []struct { | |||||
job *actions_model.ActionRunJob | |||||
rerunJobs []*actions_model.ActionRunJob | |||||
}{ | |||||
{ | |||||
job1, | |||||
[]*actions_model.ActionRunJob{job1, job2, job3, job4}, | |||||
}, | |||||
{ | |||||
job2, | |||||
[]*actions_model.ActionRunJob{job2, job3, job4}, | |||||
}, | |||||
{ | |||||
job3, | |||||
[]*actions_model.ActionRunJob{job3, job4}, | |||||
}, | |||||
{ | |||||
job4, | |||||
[]*actions_model.ActionRunJob{job4}, | |||||
}, | |||||
} | |||||
for _, tc := range testCases { | |||||
rerunJobs := GetAllRerunJobs(tc.job, jobs) | |||||
assert.ElementsMatch(t, tc.rerunJobs, rerunJobs) | |||||
} | |||||
} |