The job should always run when if is always() (#29464)
				
					
				
			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)
This commit is contained in:
		
							parent
							
								
									ff581d5a24
								
							
						
					
					
						commit
						9159842b56
					
				
					 2 changed files with 76 additions and 1 deletions
				
			
		| 
						 | 
				
			
			@ -7,12 +7,14 @@ 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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,12 +78,15 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 | 
			
		|||
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))
 | 
			
		||||
| 
						 | 
				
			
			@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
 | 
			
		|||
	return &jobStatusResolver{
 | 
			
		||||
		statuses: statuses,
 | 
			
		||||
		needs:    needs,
 | 
			
		||||
		jobMap:   jobMap,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -135,7 +141,20 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
 | 
			
		|||
			if allSucceed {
 | 
			
		||||
				ret[id] = actions_model.StatusWaiting
 | 
			
		||||
			} else {
 | 
			
		||||
				ret[id] = actions_model.StatusSkipped
 | 
			
		||||
				// 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
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,6 +70,62 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
			want: map[int64]actions_model.Status{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "with ${{ always() }} condition",
 | 
			
		||||
			jobs: actions_model.ActionJobList{
 | 
			
		||||
				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
 | 
			
		||||
				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
 | 
			
		||||
					`
 | 
			
		||||
name: test
 | 
			
		||||
on: push
 | 
			
		||||
jobs:
 | 
			
		||||
  job2:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: job1
 | 
			
		||||
    if: ${{ always() }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "always run"
 | 
			
		||||
`)},
 | 
			
		||||
			},
 | 
			
		||||
			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "with always() condition",
 | 
			
		||||
			jobs: actions_model.ActionJobList{
 | 
			
		||||
				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
 | 
			
		||||
				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
 | 
			
		||||
					`
 | 
			
		||||
name: test
 | 
			
		||||
on: push
 | 
			
		||||
jobs:
 | 
			
		||||
  job2:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: job1
 | 
			
		||||
    if: always()
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "always run"
 | 
			
		||||
`)},
 | 
			
		||||
			},
 | 
			
		||||
			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "without always() condition",
 | 
			
		||||
			jobs: actions_model.ActionJobList{
 | 
			
		||||
				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
 | 
			
		||||
				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
 | 
			
		||||
					`
 | 
			
		||||
name: test
 | 
			
		||||
on: push
 | 
			
		||||
jobs:
 | 
			
		||||
  job2:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: job1
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "not always run"
 | 
			
		||||
`)},
 | 
			
		||||
			},
 | 
			
		||||
			want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue