Skip to content

Commit 04b6f90

Browse files
Fix actions rerun bug (#35783) (#35784)
Backport #35783 Fix #35780, fix #35782 Rerunning a job or a run is only allowed when the job is done and the run is done. Related PR: #34970 https://github.com/go-gitea/gitea/blob/98ff7d077376db1225f266095788c6bd9414288a/routers/web/repo/actions/view.go#L239 We don't need to check run status again in `rerunJob` because the run status has been changed before `rerunJob`. --- In fact, the bug described in the above issues will not occur on the main branch. Because `getRunJobs` is called before updating the run. https://github.com/go-gitea/gitea/blob/98ff7d077376db1225f266095788c6bd9414288a/routers/web/repo/actions/view.go#L425-L435 So the run status that `rerunJob` checks is the old status. --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 65a3757 commit 04b6f90

File tree

3 files changed

+139
-17
lines changed

3 files changed

+139
-17
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module code.gitea.io/gitea
22

3-
go 1.25.1
3+
go 1.25.3
44

55
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
66
// But some CAs use negative serial number, just relax the check. related:

routers/web/repo/actions/view.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,12 @@ func Rerun(ctx *context_module.Context) {
412412
return
413413
}
414414

415+
// rerun is not allowed if the run is not done
416+
if !run.Status.IsDone() {
417+
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
418+
return
419+
}
420+
415421
// can not rerun job when workflow is disabled
416422
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
417423
cfg := cfgUnit.ActionsConfig()
@@ -420,23 +426,21 @@ func Rerun(ctx *context_module.Context) {
420426
return
421427
}
422428

423-
// reset run's start and stop time when it is done
424-
if run.Status.IsDone() {
425-
run.PreviousDuration = run.Duration()
426-
run.Started = 0
427-
run.Stopped = 0
428-
run.Status = actions_model.StatusWaiting
429-
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "status", "previous_duration"); err != nil {
430-
ctx.ServerError("UpdateRun", err)
431-
return
432-
}
429+
// reset run's start and stop time
430+
run.PreviousDuration = run.Duration()
431+
run.Started = 0
432+
run.Stopped = 0
433+
run.Status = actions_model.StatusWaiting
434+
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "status", "previous_duration"); err != nil {
435+
ctx.ServerError("UpdateRun", err)
436+
return
437+
}
433438

434-
if err := run.LoadAttributes(ctx); err != nil {
435-
ctx.ServerError("run.LoadAttributes", err)
436-
return
437-
}
438-
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
439+
if err := run.LoadAttributes(ctx); err != nil {
440+
ctx.ServerError("run.LoadAttributes", err)
441+
return
439442
}
443+
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
440444

441445
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
442446
if ctx.Written() {
@@ -472,7 +476,7 @@ func Rerun(ctx *context_module.Context) {
472476

473477
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
474478
status := job.Status
475-
if !status.IsDone() || !job.Run.Status.IsDone() {
479+
if !status.IsDone() {
476480
return nil
477481
}
478482

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"testing"
11+
12+
auth_model "code.gitea.io/gitea/models/auth"
13+
repo_model "code.gitea.io/gitea/models/repo"
14+
"code.gitea.io/gitea/models/unittest"
15+
user_model "code.gitea.io/gitea/models/user"
16+
17+
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
18+
)
19+
20+
func TestActionsRerun(t *testing.T) {
21+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
22+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
23+
session := loginUser(t, user2.Name)
24+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
25+
26+
apiRepo := createActionsTestRepo(t, token, "actions-rerun", false)
27+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
28+
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
29+
defer doAPIDeleteRepository(httpContext)(t)
30+
31+
runner := newMockRunner()
32+
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
33+
34+
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
35+
wfFileContent := `name: actions-rerun-workflow-1
36+
on:
37+
push:
38+
paths:
39+
- '.gitea/workflows/actions-rerun-workflow-1.yml'
40+
jobs:
41+
job1:
42+
runs-on: ubuntu-latest
43+
steps:
44+
- run: echo 'job1'
45+
job2:
46+
runs-on: ubuntu-latest
47+
needs: [job1]
48+
steps:
49+
- run: echo 'job2'
50+
`
51+
52+
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create"+wfTreePath, wfFileContent)
53+
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
54+
55+
// fetch and exec job1
56+
job1Task := runner.fetchTask(t)
57+
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
58+
runner.execTask(t, job1Task, &mockTaskOutcome{
59+
result: runnerv1.Result_RESULT_SUCCESS,
60+
})
61+
// RERUN-FAILURE: the run is not done
62+
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
63+
"_csrf": GetUserCSRFToken(t, session),
64+
})
65+
session.MakeRequest(t, req, http.StatusBadRequest)
66+
// fetch and exec job2
67+
job2Task := runner.fetchTask(t)
68+
runner.execTask(t, job2Task, &mockTaskOutcome{
69+
result: runnerv1.Result_RESULT_SUCCESS,
70+
})
71+
72+
// RERUN-1: rerun the run
73+
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
74+
"_csrf": GetUserCSRFToken(t, session),
75+
})
76+
session.MakeRequest(t, req, http.StatusOK)
77+
// fetch and exec job1
78+
job1TaskR1 := runner.fetchTask(t)
79+
runner.execTask(t, job1TaskR1, &mockTaskOutcome{
80+
result: runnerv1.Result_RESULT_SUCCESS,
81+
})
82+
// fetch and exec job2
83+
job2TaskR1 := runner.fetchTask(t)
84+
runner.execTask(t, job2TaskR1, &mockTaskOutcome{
85+
result: runnerv1.Result_RESULT_SUCCESS,
86+
})
87+
88+
// RERUN-2: rerun job1
89+
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0), map[string]string{
90+
"_csrf": GetUserCSRFToken(t, session),
91+
})
92+
session.MakeRequest(t, req, http.StatusOK)
93+
// job2 needs job1, so rerunning job1 will also rerun job2
94+
// fetch and exec job1
95+
job1TaskR2 := runner.fetchTask(t)
96+
runner.execTask(t, job1TaskR2, &mockTaskOutcome{
97+
result: runnerv1.Result_RESULT_SUCCESS,
98+
})
99+
// fetch and exec job2
100+
job2TaskR2 := runner.fetchTask(t)
101+
runner.execTask(t, job2TaskR2, &mockTaskOutcome{
102+
result: runnerv1.Result_RESULT_SUCCESS,
103+
})
104+
105+
// RERUN-3: rerun job2
106+
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1), map[string]string{
107+
"_csrf": GetUserCSRFToken(t, session),
108+
})
109+
session.MakeRequest(t, req, http.StatusOK)
110+
// only job2 will rerun
111+
// fetch and exec job2
112+
job2TaskR3 := runner.fetchTask(t)
113+
runner.execTask(t, job2TaskR3, &mockTaskOutcome{
114+
result: runnerv1.Result_RESULT_SUCCESS,
115+
})
116+
runner.fetchNoTask(t)
117+
})
118+
}

0 commit comments

Comments
 (0)