mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 14:31:02 +00:00 
			
		
		
		
	Fix #27906 According to GitHub's [documentation](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds), a job should always run when its `if` is `always()` > If you would like a job to run even if a job it is dependent on did not succeed, use the `always()` conditional expression in `jobs.<job_id>.if`. --------- Co-authored-by: Giteabot <teabot@gitea.io> (cherry picked from commit d0fe6ea4e101198911383058a2e121e384934a9c)
		
			
				
	
	
		
			162 lines
		
	
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2022 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package actions
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 
 | |
| 	actions_model "code.gitea.io/gitea/models/actions"
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/modules/graceful"
 | |
| 	"code.gitea.io/gitea/modules/queue"
 | |
| 
 | |
| 	"github.com/nektos/act/pkg/jobparser"
 | |
| 	"xorm.io/builder"
 | |
| )
 | |
| 
 | |
| var jobEmitterQueue *queue.WorkerPoolQueue[*jobUpdate]
 | |
| 
 | |
| type jobUpdate struct {
 | |
| 	RunID int64
 | |
| }
 | |
| 
 | |
| func EmitJobsIfReady(runID int64) error {
 | |
| 	err := jobEmitterQueue.Push(&jobUpdate{
 | |
| 		RunID: runID,
 | |
| 	})
 | |
| 	if errors.Is(err, queue.ErrAlreadyInQueue) {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func jobEmitterQueueHandler(items ...*jobUpdate) []*jobUpdate {
 | |
| 	ctx := graceful.GetManager().ShutdownContext()
 | |
| 	var ret []*jobUpdate
 | |
| 	for _, update := range items {
 | |
| 		if err := checkJobsOfRun(ctx, update.RunID); err != nil {
 | |
| 			ret = append(ret, update)
 | |
| 		}
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| func checkJobsOfRun(ctx context.Context, runID int64) error {
 | |
| 	jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
 | |
| 		for _, job := range jobs {
 | |
| 			idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
 | |
| 		}
 | |
| 
 | |
| 		updates := newJobStatusResolver(jobs).Resolve()
 | |
| 		for _, job := range jobs {
 | |
| 			if status, ok := updates[job.ID]; ok {
 | |
| 				job.Status = status
 | |
| 				if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
 | |
| 					return err
 | |
| 				} else if n != 1 {
 | |
| 					return fmt.Errorf("no affected for updating blocked job %v", job.ID)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	}); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	CreateCommitStatus(ctx, jobs...)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type jobStatusResolver struct {
 | |
| 	statuses map[int64]actions_model.Status
 | |
| 	needs    map[int64][]int64
 | |
| 	jobMap   map[int64]*actions_model.ActionRunJob
 | |
| }
 | |
| 
 | |
| func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 | |
| 	idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
 | |
| 	jobMap := make(map[int64]*actions_model.ActionRunJob)
 | |
| 	for _, job := range jobs {
 | |
| 		idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
 | |
| 		jobMap[job.ID] = job
 | |
| 	}
 | |
| 
 | |
| 	statuses := make(map[int64]actions_model.Status, len(jobs))
 | |
| 	needs := make(map[int64][]int64, len(jobs))
 | |
| 	for _, job := range jobs {
 | |
| 		statuses[job.ID] = job.Status
 | |
| 		for _, need := range job.Needs {
 | |
| 			for _, v := range idToJobs[need] {
 | |
| 				needs[job.ID] = append(needs[job.ID], v.ID)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return &jobStatusResolver{
 | |
| 		statuses: statuses,
 | |
| 		needs:    needs,
 | |
| 		jobMap:   jobMap,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status {
 | |
| 	ret := map[int64]actions_model.Status{}
 | |
| 	for i := 0; i < len(r.statuses); i++ {
 | |
| 		updated := r.resolve()
 | |
| 		if len(updated) == 0 {
 | |
| 			return ret
 | |
| 		}
 | |
| 		for k, v := range updated {
 | |
| 			ret[k] = v
 | |
| 			r.statuses[k] = v
 | |
| 		}
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
 | |
| 	ret := map[int64]actions_model.Status{}
 | |
| 	for id, status := range r.statuses {
 | |
| 		if status != actions_model.StatusBlocked {
 | |
| 			continue
 | |
| 		}
 | |
| 		allDone, allSucceed := true, true
 | |
| 		for _, need := range r.needs[id] {
 | |
| 			needStatus := r.statuses[need]
 | |
| 			if !needStatus.IsDone() {
 | |
| 				allDone = false
 | |
| 			}
 | |
| 			if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) {
 | |
| 				allSucceed = false
 | |
| 			}
 | |
| 		}
 | |
| 		if allDone {
 | |
| 			if allSucceed {
 | |
| 				ret[id] = actions_model.StatusWaiting
 | |
| 			} else {
 | |
| 				// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
 | |
| 				// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
 | |
| 				always := false
 | |
| 				if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
 | |
| 					_, wfJob := wfJobs[0].Job()
 | |
| 					expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
 | |
| 					always = expr == "always()"
 | |
| 				}
 | |
| 
 | |
| 				if always {
 | |
| 					ret[id] = actions_model.StatusWaiting
 | |
| 				} else {
 | |
| 					ret[id] = actions_model.StatusSkipped
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return ret
 | |
| }
 |