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:
Zettat123
2026-09-26 01:01:21 -06:00
committed by GitHub
parent f0f53a76ee
commit a15f032026
12 changed files with 263 additions and 83 deletions
+42
View File
@@ -17,6 +17,7 @@ import (
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/json"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
webhook_module "gitea.dev/modules/webhook"
"github.com/stretchr/testify/assert"
@@ -97,6 +98,47 @@ jobs:
assert.NotEmpty(t, persisted.RawConcurrency)
}
func TestPrepareRunAndInsert_JobIf(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(int64) error { return nil })()
run := insertMaxParallelRun(t, `on: push
jobs:
start:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- run: echo
skip:
if: github.event_name != 'push'
runs-on: ubuntu-latest
concurrency: skip
steps:
- run: echo
skip-caller:
if: false
uses: ./.gitea/workflows/callee.yml
invalid:
if: fromJSON('{')
runs-on: ubuntu-latest
steps:
- run: echo
`, false)
jobs := map[string]*actions_model.ActionRunJob{}
for _, job := range runJobs(t, run.ID, run.LatestAttemptID) {
jobs[job.JobID] = job
}
assert.Equal(t, actions_model.StatusWaiting, jobs["start"].Status)
assert.Equal(t, actions_model.StatusSkipped, jobs["skip"].Status)
assert.False(t, jobs["skip"].IsConcurrencyEvaluated)
assert.Equal(t, actions_model.StatusSkipped, jobs["skip-caller"].Status)
assert.Equal(t, actions_model.StatusSkipped, jobs["invalid"].Status)
summary, err := actions_model.GetActionRunJobSummary(t.Context(), run.RepoID, run.ID, run.LatestAttemptID, jobs["invalid"].ID, 0)
require.NoError(t, err)
assert.Contains(t, summary.Content, "Error when evaluating `if` for job `invalid`")
}
func TestComputeReusableCallerOutputs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()