Skip to content

Commit c7420e5

Browse files
committed
Add a doctor command to fix inconsistent run status (go-gitea#35840)
incorrectly marked as `StatusWaiting` even though all the jobs are in done status. These runs cannot be run or cancelled. This PR adds a new doctor command to fix the inconsistent run status. ``` gitea doctor check --run fix-actions-unfinished-run-status --fix ``` Thanks to @ChristopherHX for the test.
1 parent 6a55749 commit c7420e5

File tree

7 files changed

+247
-1
lines changed

7 files changed

+247
-1
lines changed

models/actions/run_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ func TestUpdateRepoRunsNumbers(t *testing.T) {
3030
err = updateRepoRunsNumbers(t.Context(), repo)
3131
assert.NoError(t, err)
3232
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
33-
assert.Equal(t, 4, repo.NumActionRuns)
33+
assert.Equal(t, 5, repo.NumActionRuns)
3434
assert.Equal(t, 3, repo.NumClosedActionRuns)
3535
}

models/fixtures/action_run.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,43 @@
139139
updated: 1683636626
140140
need_approval: 0
141141
approved_by: 0
142+
-
143+
id: 804
144+
title: "use a private action"
145+
repo_id: 60
146+
owner_id: 40
147+
workflow_id: "run.yaml"
148+
index: 189
149+
trigger_user_id: 40
150+
ref: "refs/heads/master"
151+
commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86"
152+
event: "push"
153+
trigger_event: "push"
154+
is_fork_pull_request: 0
155+
status: 1
156+
started: 1683636528
157+
stopped: 1683636626
158+
created: 1683636108
159+
updated: 1683636626
160+
need_approval: 0
161+
approved_by: 0
162+
-
163+
id: 805
164+
title: "update actions"
165+
repo_id: 4
166+
owner_id: 1
167+
workflow_id: "artifact.yaml"
168+
index: 191
169+
trigger_user_id: 1
170+
ref: "refs/heads/master"
171+
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
172+
event: "push"
173+
trigger_event: "push"
174+
is_fork_pull_request: 0
175+
status: 5
176+
started: 1683636528
177+
stopped: 1683636626
178+
created: 1683636108
179+
updated: 1683636626
180+
need_approval: 0
181+
approved_by: 0

models/fixtures/action_run_job.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,31 @@
129129
status: 5
130130
started: 1683636528
131131
stopped: 1683636626
132+
-
133+
id: 205
134+
run_id: 804
135+
repo_id: 6
136+
owner_id: 10
137+
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
138+
is_fork_pull_request: 0
139+
name: job_2
140+
attempt: 1
141+
job_id: job_2
142+
task_id: 48
143+
status: 1
144+
started: 1683636528
145+
stopped: 1683636626
146+
-
147+
id: 206
148+
run_id: 805
149+
repo_id: 4
150+
owner_id: 1
151+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
152+
is_fork_pull_request: 0
153+
name: job_2
154+
attempt: 1
155+
job_id: job_2
156+
task_id: 56
157+
status: 3
158+
started: 1683636528
159+
stopped: 1683636626

models/fixtures/action_task.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,42 @@
177177
log_length: 0
178178
log_size: 0
179179
log_expired: 0
180+
-
181+
id: 55
182+
job_id: 205
183+
attempt: 1
184+
runner_id: 1
185+
status: 6 # 6 is the status code for "running"
186+
started: 1683636528
187+
stopped: 1683636626
188+
repo_id: 6
189+
owner_id: 10
190+
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
191+
is_fork_pull_request: 0
192+
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b
193+
token_salt: ERxJGHvg3I
194+
token_last_eight: 182199eb
195+
log_filename: collaborative-owner-test/1a/49.log
196+
log_in_storage: 1
197+
log_length: 707
198+
log_size: 90179
199+
log_expired: 0
200+
-
201+
id: 56
202+
attempt: 1
203+
runner_id: 1
204+
status: 3 # 3 is the status code for "cancelled"
205+
started: 1683636528
206+
stopped: 1683636626
207+
repo_id: 4
208+
owner_id: 1
209+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
210+
is_fork_pull_request: 0
211+
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab
212+
token_salt: eeeeeeee
213+
token_last_eight: eeeeeeee
214+
log_filename: artifact-test2/2f/47.log
215+
log_in_storage: 1
216+
log_length: 707
217+
log_size: 90179
218+
log_expired: 0

models/fixtures/repo_unit.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,17 @@
733733
type: 3
734734
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
735735
created_unix: 946684810
736+
737+
-
738+
id: 111
739+
repo_id: 3
740+
type: 10
741+
config: "{}"
742+
created_unix: 946684810
743+
744+
-
745+
id: 112
746+
repo_id: 4
747+
type: 10
748+
config: "{}"
749+
created_unix: 946684810

services/doctor/actions.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ import (
77
"context"
88
"fmt"
99

10+
actions_model "code.gitea.io/gitea/models/actions"
1011
"code.gitea.io/gitea/models/db"
1112
repo_model "code.gitea.io/gitea/models/repo"
1213
unit_model "code.gitea.io/gitea/models/unit"
1314
"code.gitea.io/gitea/modules/log"
1415
"code.gitea.io/gitea/modules/optional"
16+
"code.gitea.io/gitea/modules/setting"
17+
"code.gitea.io/gitea/modules/timeutil"
1518
repo_service "code.gitea.io/gitea/services/repository"
19+
20+
"xorm.io/builder"
1621
)
1722

1823
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
@@ -59,6 +64,95 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo
5964
return nil
6065
}
6166

67+
func fixUnfinishedRunStatus(ctx context.Context, logger log.Logger, autofix bool) error {
68+
total := 0
69+
inconsistent := 0
70+
fixed := 0
71+
72+
cond := builder.In("status", []actions_model.Status{
73+
actions_model.StatusWaiting,
74+
actions_model.StatusRunning,
75+
actions_model.StatusBlocked,
76+
}).And(builder.Lt{"updated": timeutil.TimeStampNow().AddDuration(-setting.Actions.ZombieTaskTimeout)})
77+
78+
err := db.Iterate(
79+
ctx,
80+
cond,
81+
func(ctx context.Context, run *actions_model.ActionRun) error {
82+
total++
83+
84+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
85+
if err != nil {
86+
return fmt.Errorf("GetRunJobsByRunID: %w", err)
87+
}
88+
expected := actions_model.AggregateJobStatus(jobs)
89+
if expected == run.Status {
90+
return nil
91+
}
92+
93+
inconsistent++
94+
logger.Warn("Run %d (repo_id=%d, index=%d) has status %s, expected %s", run.ID, run.RepoID, run.Index, run.Status, expected)
95+
96+
if !autofix {
97+
return nil
98+
}
99+
100+
run.Started, run.Stopped = getRunTimestampsFromJobs(run, expected, jobs)
101+
run.Status = expected
102+
103+
if err := actions_model.UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
104+
return fmt.Errorf("UpdateRun: %w", err)
105+
}
106+
fixed++
107+
108+
return nil
109+
},
110+
)
111+
if err != nil {
112+
logger.Critical("Unable to iterate unfinished runs: %v", err)
113+
return err
114+
}
115+
116+
if inconsistent == 0 {
117+
logger.Info("Checked %d unfinished runs; all statuses are consistent.", total)
118+
return nil
119+
}
120+
121+
if autofix {
122+
logger.Info("Checked %d unfinished runs; fixed %d of %d runs.", total, fixed, inconsistent)
123+
} else {
124+
logger.Warn("Checked %d unfinished runs; found %d runs need to be fixed", total, inconsistent)
125+
}
126+
127+
return nil
128+
}
129+
130+
func getRunTimestampsFromJobs(run *actions_model.ActionRun, newStatus actions_model.Status, jobs actions_model.ActionJobList) (started, stopped timeutil.TimeStamp) {
131+
started = run.Started
132+
if (newStatus.IsRunning() || newStatus.IsDone()) && started.IsZero() {
133+
var earliest timeutil.TimeStamp
134+
for _, job := range jobs {
135+
if job.Started > 0 && (earliest.IsZero() || job.Started < earliest) {
136+
earliest = job.Started
137+
}
138+
}
139+
started = earliest
140+
}
141+
142+
stopped = run.Stopped
143+
if newStatus.IsDone() && stopped.IsZero() {
144+
var latest timeutil.TimeStamp
145+
for _, job := range jobs {
146+
if job.Stopped > latest {
147+
latest = job.Stopped
148+
}
149+
}
150+
stopped = latest
151+
}
152+
153+
return started, stopped
154+
}
155+
62156
func init() {
63157
Register(&Check{
64158
Title: "Disable the actions unit for all mirrors",
@@ -67,4 +161,11 @@ func init() {
67161
Run: disableMirrorActionsUnit,
68162
Priority: 9,
69163
})
164+
Register(&Check{
165+
Title: "Fix inconsistent status for unfinished actions runs",
166+
Name: "fix-actions-unfinished-run-status",
167+
IsDefault: false,
168+
Run: fixUnfinishedRunStatus,
169+
Priority: 9,
170+
})
70171
}

services/doctor/actions_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package doctor
5+
6+
import (
7+
"testing"
8+
9+
actions_model "code.gitea.io/gitea/models/actions"
10+
"code.gitea.io/gitea/models/unittest"
11+
"code.gitea.io/gitea/modules/log"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func Test_fixUnfinishedRunStatus(t *testing.T) {
17+
assert.NoError(t, unittest.PrepareTestDatabase())
18+
19+
fixUnfinishedRunStatus(t.Context(), log.GetLogger(log.DEFAULT), true)
20+
21+
// check if the run is cancelled by id
22+
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 805})
23+
assert.Equal(t, actions_model.StatusCancelled, run.Status)
24+
}

0 commit comments

Comments
 (0)