Skip to content

Commit 1f8e3b6

Browse files
committed
Fix allow cancelling runs without running jobs
1 parent d9c0f86 commit 1f8e3b6

File tree

7 files changed

+135
-21
lines changed

7 files changed

+135
-21
lines changed

models/actions/run.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
265265

266266
func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) {
267267
cancelledJobs := make([]*ActionRunJob, 0, len(jobs))
268+
269+
// List of runIDs that have no cancelled jobs
270+
runsToUpdate := map[int64]*ActionRunJob{}
271+
for _, job := range jobs {
272+
runsToUpdate[job.RunID] = job
273+
}
274+
268275
// Iterate over each job and attempt to cancel it.
269276
for _, job := range jobs {
270277
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
@@ -304,6 +311,11 @@ func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, err
304311
return cancelledJobs, fmt.Errorf("get job: %w", err)
305312
}
306313
cancelledJobs = append(cancelledJobs, updatedJob)
314+
delete(runsToUpdate, job.RunID)
315+
}
316+
317+
for runID, job := range runsToUpdate {
318+
UpdateRunStatus(ctx, job.RepoID, runID)
307319
}
308320

309321
// Return nil to indicate successful cancellation of all running and waiting jobs.

models/actions/run_job.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error)
138138
return jobs, nil
139139
}
140140

141+
func UpdateRunStatus(ctx context.Context, repoID, runID int64) error {
142+
// Other goroutines may aggregate the status of the run and update it too.
143+
// So we need load the run and its jobs before updating the run.
144+
run, err := GetRunByRepoAndID(ctx, repoID, runID)
145+
if err != nil {
146+
return err
147+
}
148+
jobs, err := GetRunJobsByRunID(ctx, runID)
149+
if err != nil {
150+
return err
151+
}
152+
run.Status = AggregateJobStatus(jobs)
153+
if run.Started.IsZero() && run.Status.IsRunning() {
154+
run.Started = timeutil.TimeStampNow()
155+
}
156+
if run.Stopped.IsZero() && run.Status.IsDone() {
157+
run.Stopped = timeutil.TimeStampNow()
158+
}
159+
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
160+
return fmt.Errorf("update run %d: %w", run.ID, err)
161+
}
162+
return nil
163+
}
164+
141165
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
142166
e := db.GetEngine(ctx)
143167

@@ -173,27 +197,8 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
173197
}
174198
}
175199

176-
{
177-
// Other goroutines may aggregate the status of the run and update it too.
178-
// So we need load the run and its jobs before updating the run.
179-
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
180-
if err != nil {
181-
return 0, err
182-
}
183-
jobs, err := GetRunJobsByRunID(ctx, job.RunID)
184-
if err != nil {
185-
return 0, err
186-
}
187-
run.Status = AggregateJobStatus(jobs)
188-
if run.Started.IsZero() && run.Status.IsRunning() {
189-
run.Started = timeutil.TimeStampNow()
190-
}
191-
if run.Stopped.IsZero() && run.Status.IsDone() {
192-
run.Stopped = timeutil.TimeStampNow()
193-
}
194-
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
195-
return 0, fmt.Errorf("update run %d: %w", run.ID, err)
196-
}
200+
if err := UpdateRunStatus(ctx, job.RepoID, job.RunID); err != nil {
201+
return 0, err
197202
}
198203

199204
return affected, nil

models/fixtures/action_run.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,23 @@
159159
updated: 1683636626
160160
need_approval: 0
161161
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,17 @@
143143
status: 1
144144
started: 1683636528
145145
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,22 @@
197197
log_length: 707
198198
log_size: 90179
199199
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,10 @@
740740
type: 10
741741
config: "{}"
742742
created_unix: 946684810
743+
744+
-
745+
id: 112
746+
repo_id: 4
747+
type: 10
748+
config: "{}"
749+
created_unix: 946684810
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
actions_model "code.gitea.io/gitea/models/actions"
13+
"code.gitea.io/gitea/models/unittest"
14+
user_model "code.gitea.io/gitea/models/user"
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
// This verifies that cancelling a run without running jobs (stuck in waiting) is updated to cancelled status
19+
func TestActionsCancelStuckWaitingRun(t *testing.T) {
20+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
21+
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
22+
session := loginUser(t, user5.Name)
23+
24+
// cancel the run by run index
25+
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user5.Name, "repo4", 191), map[string]string{
26+
"_csrf": GetUserCSRFToken(t, session),
27+
})
28+
29+
session.MakeRequest(t, req, http.StatusOK)
30+
31+
// check if the run is cancelled by id
32+
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
33+
ID: 805,
34+
})
35+
assert.Equal(t, actions_model.StatusCancelled, run.Status)
36+
})
37+
}

0 commit comments

Comments
 (0)