code.gitea.io/gitea@v1.22.3/services/actions/job_emitter.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  	"errors"
     9  	"fmt"
    10  	"strings"
    11  
    12  	actions_model "code.gitea.io/gitea/models/actions"
    13  	"code.gitea.io/gitea/models/db"
    14  	"code.gitea.io/gitea/modules/graceful"
    15  	"code.gitea.io/gitea/modules/queue"
    16  
    17  	"github.com/nektos/act/pkg/jobparser"
    18  	"xorm.io/builder"
    19  )
    20  
    21  var jobEmitterQueue *queue.WorkerPoolQueue[*jobUpdate]
    22  
    23  type jobUpdate struct {
    24  	RunID int64
    25  }
    26  
    27  func EmitJobsIfReady(runID int64) error {
    28  	err := jobEmitterQueue.Push(&jobUpdate{
    29  		RunID: runID,
    30  	})
    31  	if errors.Is(err, queue.ErrAlreadyInQueue) {
    32  		return nil
    33  	}
    34  	return err
    35  }
    36  
    37  func jobEmitterQueueHandler(items ...*jobUpdate) []*jobUpdate {
    38  	ctx := graceful.GetManager().ShutdownContext()
    39  	var ret []*jobUpdate
    40  	for _, update := range items {
    41  		if err := checkJobsOfRun(ctx, update.RunID); err != nil {
    42  			ret = append(ret, update)
    43  		}
    44  	}
    45  	return ret
    46  }
    47  
    48  func checkJobsOfRun(ctx context.Context, runID int64) error {
    49  	jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID})
    50  	if err != nil {
    51  		return err
    52  	}
    53  	if err := db.WithTx(ctx, func(ctx context.Context) error {
    54  		idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
    55  		for _, job := range jobs {
    56  			idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
    57  		}
    58  
    59  		updates := newJobStatusResolver(jobs).Resolve()
    60  		for _, job := range jobs {
    61  			if status, ok := updates[job.ID]; ok {
    62  				job.Status = status
    63  				if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
    64  					return err
    65  				} else if n != 1 {
    66  					return fmt.Errorf("no affected for updating blocked job %v", job.ID)
    67  				}
    68  			}
    69  		}
    70  		return nil
    71  	}); err != nil {
    72  		return err
    73  	}
    74  	CreateCommitStatus(ctx, jobs...)
    75  	return nil
    76  }
    77  
    78  type jobStatusResolver struct {
    79  	statuses map[int64]actions_model.Status
    80  	needs    map[int64][]int64
    81  	jobMap   map[int64]*actions_model.ActionRunJob
    82  }
    83  
    84  func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
    85  	idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
    86  	jobMap := make(map[int64]*actions_model.ActionRunJob)
    87  	for _, job := range jobs {
    88  		idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
    89  		jobMap[job.ID] = job
    90  	}
    91  
    92  	statuses := make(map[int64]actions_model.Status, len(jobs))
    93  	needs := make(map[int64][]int64, len(jobs))
    94  	for _, job := range jobs {
    95  		statuses[job.ID] = job.Status
    96  		for _, need := range job.Needs {
    97  			for _, v := range idToJobs[need] {
    98  				needs[job.ID] = append(needs[job.ID], v.ID)
    99  			}
   100  		}
   101  	}
   102  	return &jobStatusResolver{
   103  		statuses: statuses,
   104  		needs:    needs,
   105  		jobMap:   jobMap,
   106  	}
   107  }
   108  
   109  func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status {
   110  	ret := map[int64]actions_model.Status{}
   111  	for i := 0; i < len(r.statuses); i++ {
   112  		updated := r.resolve()
   113  		if len(updated) == 0 {
   114  			return ret
   115  		}
   116  		for k, v := range updated {
   117  			ret[k] = v
   118  			r.statuses[k] = v
   119  		}
   120  	}
   121  	return ret
   122  }
   123  
   124  func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
   125  	ret := map[int64]actions_model.Status{}
   126  	for id, status := range r.statuses {
   127  		if status != actions_model.StatusBlocked {
   128  			continue
   129  		}
   130  		allDone, allSucceed := true, true
   131  		for _, need := range r.needs[id] {
   132  			needStatus := r.statuses[need]
   133  			if !needStatus.IsDone() {
   134  				allDone = false
   135  			}
   136  			if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) {
   137  				allSucceed = false
   138  			}
   139  		}
   140  		if allDone {
   141  			if allSucceed {
   142  				ret[id] = actions_model.StatusWaiting
   143  			} else {
   144  				// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
   145  				// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
   146  				always := false
   147  				if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
   148  					_, wfJob := wfJobs[0].Job()
   149  					expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
   150  					always = expr == "always()"
   151  				}
   152  
   153  				if always {
   154  					ret[id] = actions_model.StatusWaiting
   155  				} else {
   156  					ret[id] = actions_model.StatusSkipped
   157  				}
   158  			}
   159  		}
   160  	}
   161  	return ret
   162  }