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:
Zettat123 2024-02-28 18:54:44 +08:00 committed by Earl Warren
parent ff581d5a24
commit 9159842b56
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
2 changed files with 76 additions and 1 deletions

View file

@ -7,12 +7,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"github.com/nektos/act/pkg/jobparser"
"xorm.io/builder" "xorm.io/builder"
) )
@ -76,12 +78,15 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
type jobStatusResolver struct { type jobStatusResolver struct {
statuses map[int64]actions_model.Status statuses map[int64]actions_model.Status
needs map[int64][]int64 needs map[int64][]int64
jobMap map[int64]*actions_model.ActionRunJob
} }
func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver { func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
jobMap := make(map[int64]*actions_model.ActionRunJob)
for _, job := range jobs { for _, job := range jobs {
idToJobs[job.JobID] = append(idToJobs[job.JobID], job) idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
jobMap[job.ID] = job
} }
statuses := make(map[int64]actions_model.Status, len(jobs)) statuses := make(map[int64]actions_model.Status, len(jobs))
@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
return &jobStatusResolver{ return &jobStatusResolver{
statuses: statuses, statuses: statuses,
needs: needs, needs: needs,
jobMap: jobMap,
} }
} }
@ -134,10 +140,23 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
if allDone { if allDone {
if allSucceed { if allSucceed {
ret[id] = actions_model.StatusWaiting 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 { } else {
ret[id] = actions_model.StatusSkipped ret[id] = actions_model.StatusSkipped
} }
} }
} }
}
return ret return ret
} }

View file

@ -70,6 +70,62 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
}, },
want: map[int64]actions_model.Status{}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {