Skip to content

Commit 3ab8ae5

Browse files
authored
Fix actions rerun bug (#35783)
Related issues: #35780, #35782 Rerunning a job or a run is only allowed when the job is done and the run is done. Related PR: #34970
1 parent 73e229e commit 3ab8ae5

File tree

2 files changed

+158
-38
lines changed

2 files changed

+158
-38
lines changed

routers/web/repo/actions/view.go

Lines changed: 40 additions & 38 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,55 +426,51 @@ func Rerun(ctx *context_module.Context) {
420426
return
421427
}
422428

423-
// check run (workflow-level) concurrency
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
424434

425-
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
426-
if ctx.Written() {
435+
vars, err := actions_model.GetVariablesOfRun(ctx, run)
436+
if err != nil {
437+
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
427438
return
428439
}
429440

430-
// reset run's start and stop time when it is done
431-
if run.Status.IsDone() {
432-
run.PreviousDuration = run.Duration()
433-
run.Started = 0
434-
run.Stopped = 0
435-
run.Status = actions_model.StatusWaiting
436-
437-
vars, err := actions_model.GetVariablesOfRun(ctx, run)
438-
if err != nil {
439-
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
441+
if run.RawConcurrency != "" {
442+
var rawConcurrency model.RawConcurrency
443+
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
444+
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
440445
return
441446
}
442447

443-
if run.RawConcurrency != "" {
444-
var rawConcurrency model.RawConcurrency
445-
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
446-
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
447-
return
448-
}
449-
450-
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
451-
if err != nil {
452-
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
453-
return
454-
}
455-
456-
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
457-
if err != nil {
458-
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
459-
return
460-
}
461-
}
462-
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
463-
ctx.ServerError("UpdateRun", err)
448+
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
449+
if err != nil {
450+
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
464451
return
465452
}
466453

467-
if err := run.LoadAttributes(ctx); err != nil {
468-
ctx.ServerError("run.LoadAttributes", err)
454+
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
455+
if err != nil {
456+
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
469457
return
470458
}
471-
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
459+
}
460+
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
461+
ctx.ServerError("UpdateRun", err)
462+
return
463+
}
464+
465+
if err := run.LoadAttributes(ctx); err != nil {
466+
ctx.ServerError("run.LoadAttributes", err)
467+
return
468+
}
469+
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
470+
471+
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
472+
if ctx.Written() {
473+
return
472474
}
473475

474476
isRunBlocked := run.Status == actions_model.StatusBlocked
@@ -501,7 +503,7 @@ func Rerun(ctx *context_module.Context) {
501503

502504
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
503505
status := job.Status
504-
if !status.IsDone() || !job.Run.Status.IsDone() {
506+
if !status.IsDone() {
505507
return nil
506508
}
507509

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)