mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Fix bugs in rerunning jobs (#29955)
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" />
This commit is contained in:
		| @@ -3626,6 +3626,7 @@ runs.scheduled = Scheduled | |||||||
| 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 | ||||||
|   | |||||||
| @@ -104,8 +104,13 @@ func List(ctx *context.Context) { | |||||||
| 				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, "${{") { | ||||||
| @@ -123,6 +128,9 @@ func List(ctx *context.Context) { | |||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 			if !hasJobWithoutNeeds { | ||||||
|  | 				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") | ||||||
|  | 			} | ||||||
| 			workflows = append(workflows, workflow) | 			workflows = append(workflows, workflow) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -303,12 +303,25 @@ func Rerun(ctx *context_module.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if jobIndexStr != "" { | 	if jobIndexStr == "" { // rerun all jobs | ||||||
| 		jobs = []*actions_model.ActionRunJob{job} | 		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 { | 	rerunJobs := actions_service.GetAllRerunJobs(job, jobs) | ||||||
| 		if err := rerunJob(ctx, j); err != nil { |  | ||||||
|  | 	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 | ||||||
| 		} | 		} | ||||||
| @@ -317,7 +330,7 @@ func Rerun(ctx *context_module.Context) { | |||||||
| 	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 | ||||||
| @@ -325,6 +338,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro | |||||||
|  |  | ||||||
| 	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 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								services/actions/rerun.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								services/actions/rerun.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | // 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 | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								services/actions/rerun_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								services/actions/rerun_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | // 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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user