feat: Add support for dynamic matrix evaluation in Gitea Actions workflows (#36564)

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>
This commit is contained in:
Pascal Zimmermann
2026-07-28 15:59:00 +00:00
committed by GitHub
co-authored by Claude silverwind Claude bircni Zettat123
parent 717db275d5
commit 5672b1c4cf
20 changed files with 1252 additions and 69 deletions
+27 -4
View File
@@ -318,6 +318,7 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att
continue
}
needs := parsedChild.Needs()
isMatrixDeferred := jobparser.HasDeferredMatrix(parsedChild)
if err := sw.SetJob(jobID, parsedChild.EraseNeeds()); err != nil {
return err
}
@@ -328,11 +329,10 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att
parsedChild.Name = util.EllipsisDisplayString(parsedChild.Name, 255)
// AttemptJobID: prefer a prior-attempt match by (JobID, Name) and fall back to a fresh allocator value for newly-appearing logical jobs.
// The two-level key disambiguates matrix instances (same JobID, different Names) and distinct jobs that legally share the same Name (different JobIDs).
// AttemptJobID: prefer a prior-attempt match and fall back to a fresh allocator value for newly-appearing logical jobs.
var attemptJobID int64
if priorChild, ok := priorChildren[jobID][parsedChild.Name]; ok {
attemptJobID = priorChild.AttemptJobID
if priorID, ok := priorAttemptJobID(priorChildren[jobID], parsedChild.Name, isMatrixDeferred); ok {
attemptJobID = priorID
} else {
attemptJobID, err = actions_model.GetNextAttemptJobID(ctx, run.ID)
if err != nil {
@@ -359,6 +359,11 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att
ParentJobID: caller.ID,
WorkflowSourceRepoID: sourceRepoID,
WorkflowSourceCommitSHA: sourceCommitSHA,
IsMatrixDeferred: isMatrixDeferred,
}
if isMatrixDeferred {
// Expansion overwrites WorkflowPayload; keep the raw payload so a rerun can re-derive the matrix.
child.DeferredMatrixPayload = payload
}
if perms := ExtractJobPermissionsFromWorkflow(sw, parsedChild); perms != nil {
child.TokenPermissions = perms
@@ -411,3 +416,21 @@ func undoExpansion(ctx context.Context, caller *actions_model.ActionRunJob) erro
}
return nil
}
// priorAttemptJobID returns the AttemptJobID a re-inserted child should reuse,
// given the prior attempt's rows of the same JobID indexed by Name.
func priorAttemptJobID(priorSameJobID map[string]*actions_model.ActionRunJob, name string, isMatrixDeferred bool) (int64, bool) {
if isMatrixDeferred {
for _, prior := range priorSameJobID {
if len(prior.DeferredMatrixPayload) > 0 {
return prior.AttemptJobID, true
}
}
return 0, false
}
prior, ok := priorSameJobID[name]
if !ok {
return 0, false
}
return prior.AttemptJobID, true
}