Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion services/actions/job_emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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))
Expand All @@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
return &jobStatusResolver{
statuses: statuses,
needs: needs,
jobMap: jobMap,
}
}

Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions services/actions/job_emitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down