mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-04 07:33:44 +09:00
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:
+24
-37
@@ -178,11 +178,7 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertRunJob builds a single run job from a parsed workflow job, evaluates its
|
||||
// job-level concurrency, inserts it, and — for a ready no-needs reusable caller —
|
||||
// inline-expands (or skips) it. It returns the inserted job, any jobs cancelled by
|
||||
// job concurrency, and whether a post-commit emitter pass is needed to resolve the
|
||||
// caller's dependents.
|
||||
// insertRunJob returns the inserted job, the jobs its concurrency cancelled, and whether a post-commit emitter pass is needed.
|
||||
func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt *actions_model.ActionRunAttempt, workflowJob *jobparser.SingleWorkflow, vars map[string]string, inputs map[string]any, slots maxParallelSlots) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob, bool, error) {
|
||||
id, job := workflowJob.Job()
|
||||
needs := job.Needs()
|
||||
@@ -236,6 +232,12 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
runJob.CallUses = job.Uses
|
||||
}
|
||||
|
||||
// a skipped job must neither cancel its group peers nor take a slot
|
||||
invalidIf, err := decideJobIf(ctx, run, runAttempt, runJob, vars)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("evaluate job if: %w", err)
|
||||
}
|
||||
|
||||
var cancelledConcurrencyJobs []*actions_model.ActionRunJob
|
||||
// check job concurrency
|
||||
if job.RawConcurrency != nil {
|
||||
@@ -245,8 +247,8 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
}
|
||||
runJob.RawConcurrency = string(rawConcurrency)
|
||||
|
||||
// do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter
|
||||
if len(needs) == 0 {
|
||||
// the job emitter evaluates it for jobs with `needs`, a skipped job never takes part
|
||||
if len(needs) == 0 && runJob.Status != actions_model.StatusSkipped {
|
||||
if err := EvaluateJobConcurrencyFillModel(ctx, run, runAttempt, runJob, vars, inputs); err != nil {
|
||||
return nil, nil, false, fmt.Errorf("evaluate job concurrency: %w", err)
|
||||
}
|
||||
@@ -270,43 +272,28 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
if err := db.Insert(ctx, runJob); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
// expand reusable caller
|
||||
var needPostCommitEmit bool
|
||||
if isReusableWorkflowCaller && runJob.Status == actions_model.StatusWaiting {
|
||||
if err := processInlineReusableCaller(ctx, run, runAttempt, runJob, vars); err != nil {
|
||||
if invalidIf != nil {
|
||||
if err := upsertJobErrorSummary(ctx, runJob, "if", invalidIf); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
// A processed caller always needs a resolver pass:
|
||||
// - if the caller is expanded, resolve its children jobs;
|
||||
// - if the caller is skipped, propagate its state to its dependents
|
||||
needPostCommitEmit = true
|
||||
}
|
||||
|
||||
return runJob, cancelledConcurrencyJobs, needPostCommitEmit, nil
|
||||
if isReusableWorkflowCaller && runJob.Status == actions_model.StatusWaiting {
|
||||
if err := expandInlineReusableCaller(ctx, run, runAttempt, runJob, vars); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// the emitter resolves an expanded caller's children and a skipped job's dependents
|
||||
return runJob, cancelledConcurrencyJobs, runJob.IsExpanded || runJob.Status == actions_model.StatusSkipped, nil
|
||||
}
|
||||
|
||||
// processInlineReusableCaller evaluates a no-needs reusable caller's own `if:` and
|
||||
// either inline-expands it into child jobs or marks it skipped.
|
||||
// (A caller with needs is Blocked and gets its `if:` evaluated by the job emitter instead.)
|
||||
func processInlineReusableCaller(ctx context.Context, run *actions_model.ActionRun, runAttempt *actions_model.ActionRunAttempt, caller *actions_model.ActionRunJob, vars map[string]string) error {
|
||||
shouldStart, err := evaluateJobIf(ctx, run, runAttempt, caller, vars, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("evaluate caller %d if: %w", caller.ID, err)
|
||||
func expandInlineReusableCaller(ctx context.Context, run *actions_model.ActionRun, runAttempt *actions_model.ActionRunAttempt, caller *actions_model.ActionRunJob, vars map[string]string) error {
|
||||
if err := expandReusableWorkflowCaller(ctx, run, runAttempt, caller, vars); err != nil {
|
||||
return fmt.Errorf("inline trigger caller %d ready: %w", caller.ID, err)
|
||||
}
|
||||
if shouldStart {
|
||||
if err := expandReusableWorkflowCaller(ctx, run, runAttempt, caller, vars); err != nil {
|
||||
return fmt.Errorf("inline trigger caller %d ready: %w", caller.ID, err)
|
||||
}
|
||||
// refresh the caller status
|
||||
if err := actions_model.RefreshReusableCallerStatus(ctx, caller); err != nil {
|
||||
return fmt.Errorf("refresh caller %d status: %w", caller.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
caller.Status = actions_model.StatusSkipped
|
||||
if _, err := actions_model.UpdateRunJob(ctx, caller, nil, "status"); err != nil {
|
||||
return fmt.Errorf("skip caller %d: %w", caller.ID, err)
|
||||
if err := actions_model.RefreshReusableCallerStatus(ctx, caller); err != nil {
|
||||
return fmt.Errorf("refresh caller %d status: %w", caller.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user