fix(actions): evaluate job-level if: before concurrency check (#39437)

Gitea doesn't evaluate a job's `if:` before checking the job's
concurrency group, which causes a job that should have been skipped to
incorrectly cancel other jobs in the same concurrency group.

This PR makes Gitea decide `if:` for every job before it becomes
waiting, including jobs without `needs` at insertion, on approval and on
rerun. A skipped job therefore no longer takes part in job concurrency
or holds a max-parallel slot, and a reusable caller whose `if:` is false
is no longer expanded on approval or rerun. An invalid `if:` skips the
job with an error summary.

After this PR, Gitea decides all jobs' `if:` expressions and sends `if:
always()` to the runner, so the runner no longer needs to evaluate a
job's `if:` again ([gitea/runner
`run_context.go`](https://gitea.com/gitea/runner/src/commit/81add274599355ec1838b6ebe45804890d40bab9/act/runner/run_context.go#L1195)).

---------

Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Zettat123andsilverwind authored and GitHub committed 2026-09-26 09:01:21 +02:00
1 parent f0f53a76ee
commit a15f032026
12 files changed
+263 -83

No files matched your search

+28 -3
View File
@@ -5,6 +5,7 @@ package actions
import (
"context"
"errors"
"fmt"
actions_model "gitea.dev/models/actions"
@@ -92,11 +93,35 @@ func pullRequestTargetBaseSHA(run *actions_model.ActionRun) (string, bool) {
return payload.PullRequest.Base.Sha, true
}
// evaluateJobIf evaluates a job's `if:`
// evaluateJobIf evaluates a job's `if:`. An invalid `if:` skips the job and is reported in its summary.
func evaluateJobIf(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, job *actions_model.ActionRunJob, vars map[string]string, allNeedsSucceed bool) (bool, error) {
shouldStart, err := resolveJobIf(ctx, run, attempt, job, vars, allNeedsSucceed)
if errors.Is(err, util.ErrInvalidArgument) {
return false, upsertJobErrorSummary(ctx, job, "if", err)
}
return shouldStart, err
}
// decideJobIf skips a waiting job whose `if:` is false before its insertion, an invalid `if:` is returned for the job's summary.
func decideJobIf(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, job *actions_model.ActionRunJob, vars map[string]string) (invalidIf, err error) {
if job.Status != actions_model.StatusWaiting {
return nil, nil
}
shouldStart, err := resolveJobIf(ctx, run, attempt, job, vars, true)
if errors.Is(err, util.ErrInvalidArgument) {
invalidIf, err = err, nil
}
if !shouldStart {
job.Status = actions_model.StatusSkipped
}
return invalidIf, err
}
// resolveJobIf evaluates a job's `if:` and returns an invalid `if:` as util.ErrInvalidArgument.
func resolveJobIf(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, job *actions_model.ActionRunJob, vars map[string]string, allNeedsSucceed bool) (bool, error) {
parsedJob, err := job.ParseJob()
if err != nil {
return false, upsertJobErrorSummary(ctx, job, "if", err)
return false, util.NewInvalidArgumentErrorf("%v", err)
}
// Empty `if:` reduces to implicit `success()` - true iff every need finished as Success.
if len(parsedJob.If.Value) == 0 {
@@ -122,7 +147,7 @@ func evaluateJobIf(ctx context.Context, run *actions_model.ActionRun, attempt *a
gitCtx["job"] = "" // github.com decides a job's `if:` before the job exists
shouldStart, err := jobparser.EvaluateJobIfExpression(job.JobID, parsedJob, gitCtx, jobResults, vars, inputs)
if err != nil {
return false, upsertJobErrorSummary(ctx, job, "if", err)
return false, util.NewInvalidArgumentErrorf("%v", err)
}
return shouldStart, nil
}