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 }