mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-02 22:39:36 +00:00
5672b1c4cf
Adds dynamic matrix evaluation to Gitea Actions: a job's
`strategy.matrix` can be built from the outputs of the jobs it needs.
```yaml
jobs:
generate:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.result }}
steps:
- id: set
run: echo "result=[1,2,3]" >> $GITHUB_OUTPUT
build:
needs: [generate]
runs-on: ubuntu-latest
strategy:
matrix:
version: ${{ fromJson(needs.generate.outputs.matrix) }}
steps:
- run: echo "building ${{ matrix.version }}"
```
Such a matrix cannot be expanded at planning time, so the job is planned
as a single placeholder and expanded by the job emitter once its needs
finish. Each combination is then gated by `if:` and concurrency as
usual.
- A matrix that resolves to no combination fails the job, as on GitHub.
- Expansion is capped at `MaxJobNumPerRun`.
- Workflows without a needs-dependent matrix are unaffected.
Fixes https://github.com/go-gitea/gitea/issues/25179
---------
Signed-off-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
Signed-off-by: ZPascal <pascal.zimmermann@theiotstudio.com>
Co-authored-by: Claude <claude-sonnet-4-5@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
101 lines
3.4 KiB
Go
101 lines
3.4 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
"gitea.dev/modules/actions/jobparser"
|
|
"gitea.dev/modules/json"
|
|
api "gitea.dev/modules/structs"
|
|
)
|
|
|
|
func getWorkflowDispatchInputsFromRun(run *actions_model.ActionRun) (map[string]any, error) {
|
|
if run.Event != "workflow_dispatch" {
|
|
return map[string]any{}, nil
|
|
}
|
|
var payload api.WorkflowDispatchPayload
|
|
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload.Inputs, nil
|
|
}
|
|
|
|
// getInputsForJob returns the `inputs.*` top-level expression context for a job's evaluation.
|
|
// - For top-level jobs, it falls back to the run's dispatch inputs (empty for non-dispatch events)
|
|
// - For reusable workflow children (and nested callers), this is the direct parent caller's CallPayload.Inputs
|
|
func getInputsForJob(ctx context.Context, run *actions_model.ActionRun, job *actions_model.ActionRunJob) (map[string]any, error) {
|
|
if job.ParentJobID == 0 {
|
|
return getWorkflowDispatchInputsFromRun(run)
|
|
}
|
|
|
|
caller, err := actions_model.GetRunJobByRunAndID(ctx, run.ID, job.ParentJobID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load caller job %d: %w", job.ParentJobID, err)
|
|
}
|
|
if caller.CallPayload == "" {
|
|
// should not happen - a child job cannot reach this point if its caller's CallPayload hasn't been evaluated
|
|
return map[string]any{}, nil
|
|
}
|
|
var p api.WorkflowCallPayload
|
|
if err := json.Unmarshal([]byte(caller.CallPayload), &p); err != nil {
|
|
return nil, fmt.Errorf("decode caller %d payload: %w", caller.ID, err)
|
|
}
|
|
if p.Inputs == nil {
|
|
return map[string]any{}, nil
|
|
}
|
|
return p.Inputs, nil
|
|
}
|
|
|
|
// evaluateJobIf evaluates a job's `if:`
|
|
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) {
|
|
parsedJob, err := job.ParseJob()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Empty `if:` reduces to implicit `success()` - true iff every need finished as Success.
|
|
if len(parsedJob.If.Value) == 0 {
|
|
return allNeedsSucceed, nil
|
|
}
|
|
jobResults, err := findJobNeedsAndFillJobResults(ctx, job)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
inputs, err := getInputsForJob(ctx, run, job)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// GenerateGiteaContext dereferences the run's repo and trigger user, so load them here instead of
|
|
// relying on whatever the caller happened to load before.
|
|
if err := run.LoadRepo(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
if err := run.LoadTriggerUser(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
gitCtx := GenerateGiteaContext(ctx, run, attempt, job)
|
|
return jobparser.EvaluateJobIfExpression(job.JobID, parsedJob, gitCtx, jobResults, vars, inputs)
|
|
}
|
|
|
|
func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*jobparser.JobResult, error) {
|
|
taskNeeds, err := FindTaskNeeds(ctx, job)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("find task needs: %w", err)
|
|
}
|
|
jobResults := make(map[string]*jobparser.JobResult, len(taskNeeds))
|
|
for jobID, taskNeed := range taskNeeds {
|
|
jobResult := &jobparser.JobResult{
|
|
Result: taskNeed.Result.String(),
|
|
Outputs: taskNeed.Outputs,
|
|
}
|
|
jobResults[jobID] = jobResult
|
|
}
|
|
jobResults[job.JobID] = &jobparser.JobResult{
|
|
Needs: job.Needs,
|
|
}
|
|
return jobResults, nil
|
|
}
|