Backport #29464 by @Zettat123 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: Zettat123 <zettat123@gmail.com>tags/v1.21.8
@@ -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) { |