Skip to content

Commit 61011f1

Browse files
authored
Add a doctor command to fix inconsistent run status (#35840) (#35845)
Backport #35840 #35783 fixes an actions rerun bug. Due to this bug, some runs may be 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 ```
1 parent 7ea9722 commit 61011f1

File tree

7 files changed

+190
-1
lines changed

7 files changed

+190
-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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,24 @@
139139
updated: 1683636626
140140
need_approval: 0
141141
approved_by: 0
142+
143+
-
144+
id: 796
145+
title: "update actions"
146+
repo_id: 4
147+
owner_id: 1
148+
workflow_id: "artifact.yaml"
149+
index: 191
150+
trigger_user_id: 1
151+
ref: "refs/heads/master"
152+
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
153+
event: "push"
154+
trigger_event: "push"
155+
is_fork_pull_request: 0
156+
status: 5
157+
started: 1683636528
158+
stopped: 1683636626
159+
created: 1683636108
160+
updated: 1683636626
161+
need_approval: 0
162+
approved_by: 0

models/fixtures/action_run_job.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,18 @@
129129
status: 5
130130
started: 1683636528
131131
stopped: 1683636626
132+
133+
-
134+
id: 205
135+
run_id: 796
136+
repo_id: 4
137+
owner_id: 1
138+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
139+
is_fork_pull_request: 0
140+
name: job_2
141+
attempt: 1
142+
job_id: job_2
143+
task_id: 55
144+
status: 3
145+
started: 1683636528
146+
stopped: 1683636626

models/fixtures/action_task.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,24 @@
177177
log_length: 0
178178
log_size: 0
179179
log_expired: 0
180+
181+
-
182+
id: 55
183+
job_id: 205
184+
attempt: 1
185+
runner_id: 1
186+
status: 3 # 3 is the status code for "cancelled"
187+
started: 1683636528
188+
stopped: 1683636626
189+
repo_id: 4
190+
owner_id: 1
191+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
192+
is_fork_pull_request: 0
193+
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab
194+
token_salt: eeeeeeee
195+
token_last_eight: eeeeeeee
196+
log_filename: artifact-test2/2f/47.log
197+
log_in_storage: 1
198+
log_length: 707
199+
log_size: 90179
200+
log_expired: 0

models/fixtures/repo_unit.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,10 @@
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: 4
740+
type: 10
741+
config: "{}"
742+
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: 796})
23+
assert.Equal(t, actions_model.StatusCancelled, run.Status)
24+
}

0 commit comments

Comments
 (0)