mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-26 05:19:44 +09:00
feat: execute post run cleanup when workflow is cancelled (#37275)
## Fixes #36983 ## Summary 1. Add transitional `Cancelling` status (between `Running` and `Cancelled`); cancel flow marks active tasks `Cancelling`, runner finalizes to `Cancelled` on terminal result. 2. Taskless jobs cancel directly (no runner to finalize). 3. Runner-protocol responses map `Cancelling` → `RESULT_CANCELLED`. 4. Run/job aggregation treats `Cancelling` as active. 5. Status mapping/aggregation tests + en-US locale added. **Problem** When a workflow was cancelled from the UI, jobs were marked cancelled immediately, which could skip post-run cleanup behavior. ## Solution Use a transitional status path: Running → Cancelling → Cancelled This allows runner finalization and cleanup path execution before final terminal state. **Testing** > 1. go test -tags "sqlite sqlite_unlock_notify" ./models/actions -run "TestAggregateJobStatus|TestStatusAsResult|TestStatusFromResult" > 2. go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 run ./models/actions/... ./routers/api/actions/runner/... ## Related - act_runner: https://gitea.com/gitea/act_runner/pulls/825 — independent; this PR's capability gate keeps legacy runners on the immediate-cancel path. The new flow activates only for runners that advertise the `cancelling` capability. Co-authored-by: Nicolas <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
co-authored by
Nicolas
silverwind
Claude
Zettat123
Giteabot
parent
ae9b34897f
commit
e7af84df72
+36
-4
@@ -193,7 +193,8 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
|
||||
}
|
||||
|
||||
var tasks []*ActionTask
|
||||
err := db.GetEngine(ctx).Where("token_last_eight = ? AND status = ?", lastEight, StatusRunning).Find(&tasks)
|
||||
// Cancelling tasks are still authenticating — post-run cleanup steps need API access (artifact uploads, cache saves, etc.) before the runner finalizes the task.
|
||||
err := db.GetEngine(ctx).Where("token_last_eight = ? AND status IN (?, ?)", lastEight, StatusRunning, StatusCancelling).Find(&tasks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(tasks) == 0 {
|
||||
@@ -374,7 +375,12 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
|
||||
// state.Result is not unspecified means the task is finished
|
||||
if state.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
task.Status = Status(state.Result)
|
||||
if task.Status == StatusCancelling {
|
||||
// The runner may report SUCCESS/FAILURE for the cleanup phase; preserve user intent.
|
||||
task.Status = StatusCancelled
|
||||
} else {
|
||||
task.Status = StatusFromResult(state.Result)
|
||||
}
|
||||
task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix())
|
||||
if err := UpdateTask(ctx, task, "status", "stopped"); err != nil {
|
||||
return nil, err
|
||||
@@ -409,7 +415,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
step.Stopped = convertTimestamp(v.StoppedAt)
|
||||
}
|
||||
if result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
step.Status = Status(result)
|
||||
step.Status = StatusFromResult(result)
|
||||
} else if step.Started != 0 {
|
||||
step.Status = StatusRunning
|
||||
}
|
||||
@@ -423,7 +429,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
}
|
||||
|
||||
func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||
if !status.IsDone() {
|
||||
if !status.IsDone() && status != StatusCancelling {
|
||||
return fmt.Errorf("cannot stop task with status %v", status)
|
||||
}
|
||||
e := db.GetEngine(ctx)
|
||||
@@ -439,6 +445,32 @@ func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||
}
|
||||
|
||||
now := timeutil.TimeStampNow()
|
||||
if status == StatusCancelling {
|
||||
runner, err := GetRunnerByID(ctx, task.RunnerID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
status = StatusCancelled
|
||||
} else if !runner.HasCancellingSupport {
|
||||
status = StatusCancelled
|
||||
}
|
||||
}
|
||||
|
||||
if status == StatusCancelling {
|
||||
task.Status = StatusCancelling
|
||||
|
||||
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
||||
ID: task.JobID,
|
||||
RepoID: task.RepoID,
|
||||
Status: StatusCancelling,
|
||||
}, nil, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return UpdateTask(ctx, task, "status")
|
||||
}
|
||||
|
||||
task.Status = status
|
||||
task.Stopped = now
|
||||
if _, err := UpdateRunJob(ctx, &ActionRunJob{
|
||||
|
||||
Reference in New Issue
Block a user