Skip to content

Commit 5e58639

Browse files
Zettat123GiteaBot
authored andcommitted
Fix bugs in rerunning jobs (go-gitea#29955)
Fix go-gitea#28761 Fix go-gitea#27884 Fix go-gitea#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 (go-gitea#28761 (comment)). 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" />
1 parent 6ef986d commit 5e58639

File tree

5 files changed

+117
-6
lines changed

5 files changed

+117
-6
lines changed

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3529,6 +3529,7 @@ runs.scheduled = Scheduled
35293529
runs.pushed_by = pushed by
35303530
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
35313531
runs.no_matching_online_runner_helper = No matching online runner with label: %s
3532+
runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
35323533
runs.actor = Actor
35333534
runs.status = Status
35343535
runs.actors_no_select = All actors

routers/web/repo/actions/actions.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,13 @@ func List(ctx *context.Context) {
105105
workflows = append(workflows, workflow)
106106
continue
107107
}
108-
// Check whether have matching runner
108+
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
109+
hasJobWithoutNeeds := false
110+
// Check whether have matching runner and a job without "needs"
109111
for _, j := range wf.Jobs {
112+
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
113+
hasJobWithoutNeeds = true
114+
}
110115
runsOnList := j.RunsOn()
111116
for _, ro := range runsOnList {
112117
if strings.Contains(ro, "${{") {
@@ -124,6 +129,9 @@ func List(ctx *context.Context) {
124129
break
125130
}
126131
}
132+
if !hasJobWithoutNeeds {
133+
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
134+
}
127135
workflows = append(workflows, workflow)
128136
}
129137
}

routers/web/repo/actions/view.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,25 @@ func Rerun(ctx *context_module.Context) {
289289
return
290290
}
291291

292-
if jobIndexStr != "" {
293-
jobs = []*actions_model.ActionRunJob{job}
292+
if jobIndexStr == "" { // rerun all jobs
293+
for _, j := range jobs {
294+
// if the job has needs, it should be set to "blocked" status to wait for other jobs
295+
shouldBlock := len(j.Needs) > 0
296+
if err := rerunJob(ctx, j, shouldBlock); err != nil {
297+
ctx.Error(http.StatusInternalServerError, err.Error())
298+
return
299+
}
300+
}
301+
ctx.JSON(http.StatusOK, struct{}{})
302+
return
294303
}
295304

296-
for _, j := range jobs {
297-
if err := rerunJob(ctx, j); err != nil {
305+
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
306+
307+
for _, j := range rerunJobs {
308+
// jobs other than the specified one should be set to "blocked" status
309+
shouldBlock := j.JobID != job.JobID
310+
if err := rerunJob(ctx, j, shouldBlock); err != nil {
298311
ctx.Error(http.StatusInternalServerError, err.Error())
299312
return
300313
}
@@ -303,14 +316,17 @@ func Rerun(ctx *context_module.Context) {
303316
ctx.JSON(http.StatusOK, struct{}{})
304317
}
305318

306-
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
319+
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
307320
status := job.Status
308321
if !status.IsDone() {
309322
return nil
310323
}
311324

312325
job.TaskID = 0
313326
job.Status = actions_model.StatusWaiting
327+
if shouldBlock {
328+
job.Status = actions_model.StatusBlocked
329+
}
314330
job.Started = 0
315331
job.Stopped = 0
316332

services/actions/rerun.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
actions_model "code.gitea.io/gitea/models/actions"
8+
"code.gitea.io/gitea/modules/container"
9+
)
10+
11+
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
12+
func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
13+
rerunJobs := []*actions_model.ActionRunJob{job}
14+
rerunJobsIDSet := make(container.Set[string])
15+
rerunJobsIDSet.Add(job.JobID)
16+
17+
for {
18+
found := false
19+
for _, j := range allJobs {
20+
if rerunJobsIDSet.Contains(j.JobID) {
21+
continue
22+
}
23+
for _, need := range j.Needs {
24+
if rerunJobsIDSet.Contains(need) {
25+
found = true
26+
rerunJobs = append(rerunJobs, j)
27+
rerunJobsIDSet.Add(j.JobID)
28+
break
29+
}
30+
}
31+
}
32+
if !found {
33+
break
34+
}
35+
}
36+
37+
return rerunJobs
38+
}

services/actions/rerun_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"testing"
8+
9+
actions_model "code.gitea.io/gitea/models/actions"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestGetAllRerunJobs(t *testing.T) {
15+
job1 := &actions_model.ActionRunJob{JobID: "job1"}
16+
job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}}
17+
job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}}
18+
job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}}
19+
20+
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
21+
22+
testCases := []struct {
23+
job *actions_model.ActionRunJob
24+
rerunJobs []*actions_model.ActionRunJob
25+
}{
26+
{
27+
job1,
28+
[]*actions_model.ActionRunJob{job1, job2, job3, job4},
29+
},
30+
{
31+
job2,
32+
[]*actions_model.ActionRunJob{job2, job3, job4},
33+
},
34+
{
35+
job3,
36+
[]*actions_model.ActionRunJob{job3, job4},
37+
},
38+
{
39+
job4,
40+
[]*actions_model.ActionRunJob{job4},
41+
},
42+
}
43+
44+
for _, tc := range testCases {
45+
rerunJobs := GetAllRerunJobs(tc.job, jobs)
46+
assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
47+
}
48+
}

0 commit comments

Comments
 (0)