code.gitea.io/gitea@v1.21.7/models/actions/run_job.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package actions
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"slices"
    10  	"time"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	"code.gitea.io/gitea/modules/timeutil"
    14  	"code.gitea.io/gitea/modules/util"
    15  
    16  	"xorm.io/builder"
    17  )
    18  
    19  // ActionRunJob represents a job of a run
    20  type ActionRunJob struct {
    21  	ID                int64
    22  	RunID             int64      `xorm:"index"`
    23  	Run               *ActionRun `xorm:"-"`
    24  	RepoID            int64      `xorm:"index"`
    25  	OwnerID           int64      `xorm:"index"`
    26  	CommitSHA         string     `xorm:"index"`
    27  	IsForkPullRequest bool
    28  	Name              string `xorm:"VARCHAR(255)"`
    29  	Attempt           int64
    30  	WorkflowPayload   []byte
    31  	JobID             string   `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
    32  	Needs             []string `xorm:"JSON TEXT"`
    33  	RunsOn            []string `xorm:"JSON TEXT"`
    34  	TaskID            int64    // the latest task of the job
    35  	Status            Status   `xorm:"index"`
    36  	Started           timeutil.TimeStamp
    37  	Stopped           timeutil.TimeStamp
    38  	Created           timeutil.TimeStamp `xorm:"created"`
    39  	Updated           timeutil.TimeStamp `xorm:"updated index"`
    40  }
    41  
    42  func init() {
    43  	db.RegisterModel(new(ActionRunJob))
    44  }
    45  
    46  func (job *ActionRunJob) Duration() time.Duration {
    47  	return calculateDuration(job.Started, job.Stopped, job.Status)
    48  }
    49  
    50  func (job *ActionRunJob) LoadRun(ctx context.Context) error {
    51  	if job.Run == nil {
    52  		run, err := GetRunByID(ctx, job.RunID)
    53  		if err != nil {
    54  			return err
    55  		}
    56  		job.Run = run
    57  	}
    58  	return nil
    59  }
    60  
    61  // LoadAttributes load Run if not loaded
    62  func (job *ActionRunJob) LoadAttributes(ctx context.Context) error {
    63  	if job == nil {
    64  		return nil
    65  	}
    66  
    67  	if err := job.LoadRun(ctx); err != nil {
    68  		return err
    69  	}
    70  
    71  	return job.Run.LoadAttributes(ctx)
    72  }
    73  
    74  func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
    75  	var job ActionRunJob
    76  	has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job)
    77  	if err != nil {
    78  		return nil, err
    79  	} else if !has {
    80  		return nil, fmt.Errorf("run job with id %d: %w", id, util.ErrNotExist)
    81  	}
    82  
    83  	return &job, nil
    84  }
    85  
    86  func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) {
    87  	var jobs []*ActionRunJob
    88  	if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil {
    89  		return nil, err
    90  	}
    91  	return jobs, nil
    92  }
    93  
    94  func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
    95  	e := db.GetEngine(ctx)
    96  
    97  	sess := e.ID(job.ID)
    98  	if len(cols) > 0 {
    99  		sess.Cols(cols...)
   100  	}
   101  
   102  	if cond != nil {
   103  		sess.Where(cond)
   104  	}
   105  
   106  	affected, err := sess.Update(job)
   107  	if err != nil {
   108  		return 0, err
   109  	}
   110  
   111  	if affected == 0 || (!slices.Contains(cols, "status") && job.Status == 0) {
   112  		return affected, nil
   113  	}
   114  
   115  	if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() {
   116  		// if the status of job changes to waiting again, increase tasks version.
   117  		if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
   118  			return 0, err
   119  		}
   120  	}
   121  
   122  	if job.RunID == 0 {
   123  		var err error
   124  		if job, err = GetRunJobByID(ctx, job.ID); err != nil {
   125  			return 0, err
   126  		}
   127  	}
   128  
   129  	{
   130  		// Other goroutines may aggregate the status of the run and update it too.
   131  		// So we need load the run and its jobs before updating the run.
   132  		run, err := GetRunByID(ctx, job.RunID)
   133  		if err != nil {
   134  			return 0, err
   135  		}
   136  		jobs, err := GetRunJobsByRunID(ctx, job.RunID)
   137  		if err != nil {
   138  			return 0, err
   139  		}
   140  		run.Status = aggregateJobStatus(jobs)
   141  		if run.Started.IsZero() && run.Status.IsRunning() {
   142  			run.Started = timeutil.TimeStampNow()
   143  		}
   144  		if run.Stopped.IsZero() && run.Status.IsDone() {
   145  			run.Stopped = timeutil.TimeStampNow()
   146  		}
   147  		if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
   148  			return 0, fmt.Errorf("update run %d: %w", run.ID, err)
   149  		}
   150  	}
   151  
   152  	return affected, nil
   153  }
   154  
   155  func aggregateJobStatus(jobs []*ActionRunJob) Status {
   156  	allDone := true
   157  	allWaiting := true
   158  	hasFailure := false
   159  	for _, job := range jobs {
   160  		if !job.Status.IsDone() {
   161  			allDone = false
   162  		}
   163  		if job.Status != StatusWaiting && !job.Status.IsDone() {
   164  			allWaiting = false
   165  		}
   166  		if job.Status == StatusFailure || job.Status == StatusCancelled {
   167  			hasFailure = true
   168  		}
   169  	}
   170  	if allDone {
   171  		if hasFailure {
   172  			return StatusFailure
   173  		}
   174  		return StatusSuccess
   175  	}
   176  	if allWaiting {
   177  		return StatusWaiting
   178  	}
   179  	return StatusRunning
   180  }