2023-01-31 09:45:19 +08:00
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"fmt"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
2025-03-10 23:58:48 +01:00
repo_model "code.gitea.io/gitea/models/repo"
2023-01-31 09:45:19 +08:00
"code.gitea.io/gitea/modules/actions"
2026-05-15 23:02:14 -06:00
"code.gitea.io/gitea/modules/container"
2023-01-31 09:45:19 +08:00
"code.gitea.io/gitea/modules/log"
2023-10-02 23:09:26 +02:00
"code.gitea.io/gitea/modules/setting"
2023-01-31 09:45:19 +08:00
"code.gitea.io/gitea/modules/timeutil"
2025-10-10 12:58:55 -06:00
"code.gitea.io/gitea/modules/util"
2025-03-10 23:58:48 +01:00
webhook_module "code.gitea.io/gitea/modules/webhook"
2025-03-11 18:40:38 +01:00
notify_service "code.gitea.io/gitea/services/notify"
2023-01-31 09:45:19 +08:00
)
// StopZombieTasks stops the task which have running status, but haven't been updated for a long time
func StopZombieTasks ( ctx context . Context ) error {
return stopTasks ( ctx , actions_model . FindTaskOptions {
Status : actions_model . StatusRunning ,
2023-10-02 23:09:26 +02:00
UpdatedBefore : timeutil . TimeStamp ( time . Now ( ) . Add ( - setting . Actions . ZombieTaskTimeout ) . Unix ( ) ) ,
2023-01-31 09:45:19 +08:00
} )
}
// StopEndlessTasks stops the tasks which have running status and continuous updates, but don't end for a long time
func StopEndlessTasks ( ctx context . Context ) error {
return stopTasks ( ctx , actions_model . FindTaskOptions {
Status : actions_model . StatusRunning ,
2023-10-02 23:09:26 +02:00
StartedBefore : timeutil . TimeStamp ( time . Now ( ) . Add ( - setting . Actions . EndlessTaskTimeout ) . Unix ( ) ) ,
2023-01-31 09:45:19 +08:00
} )
}
2025-03-10 23:58:48 +01:00
func notifyWorkflowJobStatusUpdate ( ctx context . Context , jobs [ ] * actions_model . ActionRunJob ) {
2025-10-12 14:23:37 +02:00
if len ( jobs ) == 0 {
return
}
2026-03-26 12:48:04 -06:00
// The input jobs may belong to different runs, so track each affected run.
runs := make ( map [ int64 ] * actions_model . ActionRun , len ( jobs ) )
2025-10-12 14:23:37 +02:00
for _ , job := range jobs {
if err := job . LoadAttributes ( ctx ) ; err != nil {
log . Error ( "Failed to load job attributes: %v" , err )
continue
2025-03-11 18:40:38 +01:00
}
2025-10-12 14:23:37 +02:00
CreateCommitStatusForRunJobs ( ctx , job . Run , job )
notify_service . WorkflowJobStatusUpdate ( ctx , job . Run . Repo , job . Run . TriggerUser , job , nil )
2026-03-26 12:48:04 -06:00
if _ , ok := runs [ job . RunID ] ; ! ok {
runs [ job . RunID ] = job . Run
}
2025-10-12 14:23:37 +02:00
}
2026-03-26 12:48:04 -06:00
for _ , run := range runs {
notify_service . WorkflowRunStatusUpdate ( ctx , run . Repo , run . TriggerUser , run )
2025-03-10 23:58:48 +01:00
}
}
func CancelPreviousJobs ( ctx context . Context , repoID int64 , ref , workflowID string , event webhook_module . HookEventType ) error {
jobs , err := actions_model . CancelPreviousJobs ( ctx , repoID , ref , workflowID , event )
notifyWorkflowJobStatusUpdate ( ctx , jobs )
2026-05-15 23:02:14 -06:00
if len ( jobs ) > 0 {
actions_model . UpdateRepoRunsNumbers ( ctx , repoID )
}
2025-10-10 12:58:55 -06:00
EmitJobsIfReadyByJobs ( jobs )
2025-03-10 23:58:48 +01:00
return err
}
func CleanRepoScheduleTasks ( ctx context . Context , repo * repo_model . Repository ) error {
jobs , err := actions_model . CleanRepoScheduleTasks ( ctx , repo )
notifyWorkflowJobStatusUpdate ( ctx , jobs )
2026-05-15 23:02:14 -06:00
if len ( jobs ) > 0 {
actions_model . UpdateRepoRunsNumbers ( ctx , repo . ID )
}
2025-10-10 12:58:55 -06:00
EmitJobsIfReadyByJobs ( jobs )
2025-03-10 23:58:48 +01:00
return err
}
2025-10-10 12:58:55 -06:00
func shouldBlockJobByConcurrency ( ctx context . Context , job * actions_model . ActionRunJob ) ( bool , error ) {
if job . RawConcurrency != "" && ! job . IsConcurrencyEvaluated {
// when the job depends on other jobs, we cannot evaluate its concurrency, so it should be blocked and will be evaluated again when its dependencies are done
return true , nil
}
if job . ConcurrencyGroup == "" || job . ConcurrencyCancel {
return false , nil
}
runs , jobs , err := actions_model . GetConcurrentRunsAndJobs ( ctx , job . RepoID , job . ConcurrencyGroup , [ ] actions_model . Status { actions_model . StatusRunning } )
if err != nil {
return false , fmt . Errorf ( "GetConcurrentRunsAndJobs: %w" , err )
}
return len ( runs ) > 0 || len ( jobs ) > 0 , nil
}
// PrepareToStartJobWithConcurrency prepares a job to start by its evaluated concurrency group and cancelling previous jobs if necessary.
// It returns the new status of the job (either StatusBlocked or StatusWaiting) and any error encountered during the process.
func PrepareToStartJobWithConcurrency ( ctx context . Context , job * actions_model . ActionRunJob ) ( actions_model . Status , error ) {
shouldBlock , err := shouldBlockJobByConcurrency ( ctx , job )
if err != nil {
return actions_model . StatusBlocked , err
}
// even if the current job is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group
jobs , err := actions_model . CancelPreviousJobsByJobConcurrency ( ctx , job )
if err != nil {
return actions_model . StatusBlocked , fmt . Errorf ( "CancelPreviousJobsByJobConcurrency: %w" , err )
}
notifyWorkflowJobStatusUpdate ( ctx , jobs )
return util . Iif ( shouldBlock , actions_model . StatusBlocked , actions_model . StatusWaiting ) , nil
}
func shouldBlockRunByConcurrency ( ctx context . Context , actionRun * actions_model . ActionRun ) ( bool , error ) {
if actionRun . ConcurrencyGroup == "" || actionRun . ConcurrencyCancel {
return false , nil
}
runs , jobs , err := actions_model . GetConcurrentRunsAndJobs ( ctx , actionRun . RepoID , actionRun . ConcurrencyGroup , [ ] actions_model . Status { actions_model . StatusRunning } )
if err != nil {
return false , fmt . Errorf ( "find concurrent runs and jobs: %w" , err )
}
return len ( runs ) > 0 || len ( jobs ) > 0 , nil
}
// PrepareToStartRunWithConcurrency prepares a run to start by its evaluated concurrency group and cancelling previous jobs if necessary.
// It returns the new status of the run (either StatusBlocked or StatusWaiting) and any error encountered during the process.
func PrepareToStartRunWithConcurrency ( ctx context . Context , run * actions_model . ActionRun ) ( actions_model . Status , error ) {
shouldBlock , err := shouldBlockRunByConcurrency ( ctx , run )
if err != nil {
return actions_model . StatusBlocked , err
}
// even if the current run is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group
jobs , err := actions_model . CancelPreviousJobsByRunConcurrency ( ctx , run )
if err != nil {
return actions_model . StatusBlocked , fmt . Errorf ( "CancelPreviousJobsByRunConcurrency: %w" , err )
}
notifyWorkflowJobStatusUpdate ( ctx , jobs )
return util . Iif ( shouldBlock , actions_model . StatusBlocked , actions_model . StatusWaiting ) , nil
}
2023-01-31 09:45:19 +08:00
func stopTasks ( ctx context . Context , opts actions_model . FindTaskOptions ) error {
2023-11-24 11:49:41 +08:00
tasks , err := db . Find [ actions_model . ActionTask ] ( ctx , opts )
2023-01-31 09:45:19 +08:00
if err != nil {
return fmt . Errorf ( "find tasks: %w" , err )
}
2023-03-04 15:12:37 +08:00
jobs := make ( [ ] * actions_model . ActionRunJob , 0 , len ( tasks ) )
2023-01-31 09:45:19 +08:00
for _ , task := range tasks {
if err := db . WithTx ( ctx , func ( ctx context . Context ) error {
if err := actions_model . StopTask ( ctx , task . ID , actions_model . StatusFailure ) ; err != nil {
return err
}
if err := task . LoadJob ( ctx ) ; err != nil {
return err
}
2023-03-04 15:12:37 +08:00
jobs = append ( jobs , task . Job )
return nil
2023-01-31 09:45:19 +08:00
} ) ; err != nil {
log . Warn ( "Cannot stop task %v: %v" , task . ID , err )
2023-06-29 01:07:29 +08:00
continue
}
remove , err := actions . TransferLogs ( ctx , task . LogFilename )
if err != nil {
2023-01-31 09:45:19 +08:00
log . Warn ( "Cannot transfer logs of task %v: %v" , task . ID , err )
2023-06-29 01:07:29 +08:00
continue
}
task . LogInStorage = true
if err := actions_model . UpdateTask ( ctx , task , "log_in_storage" ) ; err != nil {
log . Warn ( "Cannot update task %v: %v" , task . ID , err )
continue
2023-01-31 09:45:19 +08:00
}
2023-06-29 01:07:29 +08:00
remove ( )
2023-01-31 09:45:19 +08:00
}
2023-03-04 15:12:37 +08:00
2025-03-10 23:58:48 +01:00
notifyWorkflowJobStatusUpdate ( ctx , jobs )
2026-05-15 23:02:14 -06:00
// Recompute counters post-commit for every repo whose runs may have flipped done-status.
reconcileRepos := make ( container . Set [ int64 ] )
for _ , job := range jobs {
reconcileRepos . Add ( job . RepoID )
}
for repoID := range reconcileRepos {
actions_model . UpdateRepoRunsNumbers ( ctx , repoID )
}
2025-10-10 12:58:55 -06:00
EmitJobsIfReadyByJobs ( jobs )
2023-03-04 15:12:37 +08:00
2023-01-31 09:45:19 +08:00
return nil
}
2025-08-25 00:30:56 +08:00
// CancelAbandonedJobs cancels jobs that have not been picked by any runner for a long time
2023-01-31 09:45:19 +08:00
func CancelAbandonedJobs ( ctx context . Context ) error {
2023-11-24 11:49:41 +08:00
jobs , err := db . Find [ actions_model . ActionRunJob ] ( ctx , actions_model . FindRunJobOptions {
2023-01-31 09:45:19 +08:00
Statuses : [ ] actions_model . Status { actions_model . StatusWaiting , actions_model . StatusBlocked } ,
2025-10-10 12:58:55 -06:00
UpdatedBefore : timeutil . TimeStampNow ( ) . AddDuration ( - setting . Actions . AbandonedJobTimeout ) ,
2023-01-31 09:45:19 +08:00
} )
if err != nil {
log . Warn ( "find abandoned tasks: %v" , err )
return err
}
now := timeutil . TimeStampNow ( )
2025-08-25 00:30:56 +08:00
// Collect one job per run to send workflow run status update
updatedRuns := map [ int64 ] * actions_model . ActionRunJob { }
2025-10-10 12:58:55 -06:00
updatedJobs := [ ] * actions_model . ActionRunJob { }
2026-05-15 23:02:14 -06:00
updatedRepoIDs := make ( container . Set [ int64 ] )
2025-08-25 00:30:56 +08:00
2023-01-31 09:45:19 +08:00
for _ , job := range jobs {
job . Status = actions_model . StatusCancelled
job . Stopped = now
2025-03-11 18:40:38 +01:00
updated := false
2023-01-31 09:45:19 +08:00
if err := db . WithTx ( ctx , func ( ctx context . Context ) error {
2025-03-11 18:40:38 +01:00
n , err := actions_model . UpdateRunJob ( ctx , job , nil , "status" , "stopped" )
2025-08-25 00:30:56 +08:00
if err != nil {
return err
}
if err := job . LoadAttributes ( ctx ) ; err != nil {
return err
}
updated = n > 0
if updated && job . Run . Status . IsDone ( ) {
updatedRuns [ job . RunID ] = job
2026-05-15 23:02:14 -06:00
updatedRepoIDs . Add ( job . RepoID )
2025-08-25 00:30:56 +08:00
}
return nil
2023-01-31 09:45:19 +08:00
} ) ; err != nil {
log . Warn ( "cancel abandoned job %v: %v" , job . ID , err )
// go on
}
2025-10-12 14:23:37 +02:00
if job . Run == nil || job . Run . Repo == nil {
continue // error occurs during loading attributes, the following code that depends on "Run.Repo" will fail, so ignore and skip
}
CreateCommitStatusForRunJobs ( ctx , job . Run , job )
2025-03-11 18:40:38 +01:00
if updated {
2025-10-10 12:58:55 -06:00
updatedJobs = append ( updatedJobs , job )
2025-03-11 18:40:38 +01:00
notify_service . WorkflowJobStatusUpdate ( ctx , job . Run . Repo , job . Run . TriggerUser , job , nil )
}
2023-01-31 09:45:19 +08:00
}
2025-08-25 00:30:56 +08:00
for _ , job := range updatedRuns {
notify_service . WorkflowRunStatusUpdate ( ctx , job . Run . Repo , job . Run . TriggerUser , job . Run )
}
2025-10-10 12:58:55 -06:00
EmitJobsIfReadyByJobs ( updatedJobs )
2025-08-25 00:30:56 +08:00
2026-05-15 23:02:14 -06:00
for repoID := range updatedRepoIDs {
actions_model . UpdateRepoRunsNumbers ( ctx , repoID )
}
2023-01-31 09:45:19 +08:00
return nil
}