Fix #25897
Fix #30322
#29464 cannot handle some complex `if` conditions correctly because it
only checks `always()` literally. In fact, it's not easy to evaluate the
`if` condition on the Gitea side because evaluating it requires a series
of contexts. But act_runner is able to evaluate the `if` condition
before running the job (for more information, see
[`gitea/act`](517d11c671/pkg/runner/run_context.go (L739-L753)))
. So we can use act_runner to check the `if` condition.
In this PR, how to handle a blocked job depends on its `needs` and `if`:
- If not all jobs in `needs` completed successfully and the job's `if`
is empty, set the job status to `StatusSkipped`
- In other cases, the job status will be set to `StatusWaiting`, and
then act_runner will check the `if` condition and run the job if the
condition is met
(cherry picked from commit 31a0c4dfb4156a7b4d856cceae1e61c7fc1a4a1b)
		
	
			
		
			
				
	
	
		
			136 lines
		
	
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			136 lines
		
	
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package actions
 | 
						|
 | 
						|
import (
 | 
						|
	"testing"
 | 
						|
 | 
						|
	actions_model "code.gitea.io/gitea/models/actions"
 | 
						|
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
)
 | 
						|
 | 
						|
func Test_jobStatusResolver_Resolve(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name string
 | 
						|
		jobs actions_model.ActionJobList
 | 
						|
		want map[int64]actions_model.Status
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "no blocked",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "1", Status: actions_model.StatusWaiting, Needs: []string{}},
 | 
						|
				{ID: 2, JobID: "2", Status: actions_model.StatusWaiting, Needs: []string{}},
 | 
						|
				{ID: 3, JobID: "3", Status: actions_model.StatusWaiting, Needs: []string{}},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "single blocked",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "1", Status: actions_model.StatusSuccess, Needs: []string{}},
 | 
						|
				{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
 | 
						|
				{ID: 3, JobID: "3", Status: actions_model.StatusWaiting, Needs: []string{}},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{
 | 
						|
				2: actions_model.StatusWaiting,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "multiple blocked",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "1", Status: actions_model.StatusSuccess, Needs: []string{}},
 | 
						|
				{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
 | 
						|
				{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{
 | 
						|
				2: actions_model.StatusWaiting,
 | 
						|
				3: actions_model.StatusWaiting,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "chain blocked",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "1", Status: actions_model.StatusFailure, Needs: []string{}},
 | 
						|
				{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
 | 
						|
				{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{
 | 
						|
				2: actions_model.StatusSkipped,
 | 
						|
				3: actions_model.StatusSkipped,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "loop need",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "1", Status: actions_model.StatusBlocked, Needs: []string{"3"}},
 | 
						|
				{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
 | 
						|
				{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "`if` is not empty and all jobs in `needs` completed successfully",
 | 
						|
			jobs: actions_model.ActionJobList{
 | 
						|
				{ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, 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() && needs.job1.result == 'success' }}
 | 
						|
    steps:
 | 
						|
      - run: echo "will be checked by act_runner"
 | 
						|
`)},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "`if` is not empty and not all jobs in `needs` completed successfully",
 | 
						|
			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() && needs.job1.result == 'failure' }}
 | 
						|
    steps:
 | 
						|
      - run: echo "will be checked by act_runner"
 | 
						|
`)},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "`if` is empty and not all jobs in `needs` completed successfully",
 | 
						|
			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 "should be skipped"
 | 
						|
`)},
 | 
						|
			},
 | 
						|
			want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			r := newJobStatusResolver(tt.jobs)
 | 
						|
			assert.Equal(t, tt.want, r.Resolve())
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |