aboutsummaryrefslogtreecommitdiffstats
path: root/services/actions
diff options
context:
space:
mode:
authorZettat123 <zettat123@gmail.com>2024-02-28 18:54:44 +0800
committerGitHub <noreply@github.com>2024-02-28 11:54:44 +0100
commitd0fe6ea4e101198911383058a2e121e384934a9c (patch)
tree80389a23d0ac0d6ea0dc31220dfb02e8b10ad77b /services/actions
parentd557fbc5a715a1920a2860cb04ae6c8fe2225182 (diff)
downloadgitea-d0fe6ea4e101198911383058a2e121e384934a9c.tar.gz
gitea-d0fe6ea4e101198911383058a2e121e384934a9c.zip
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>
Diffstat (limited to 'services/actions')
-rw-r--r--services/actions/job_emitter.go21
-rw-r--r--services/actions/job_emitter_test.go56
2 files changed, 76 insertions, 1 deletions
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index fe39312386..d2bbbd9a7c 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -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
+ }
}
}
}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index e81aa61d80..038df7d4f8 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -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) {