mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-04 14:17:38 +00:00
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:
committed by
GitHub
parent
717db275d5
commit
5672b1c4cf
@@ -13,6 +13,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -72,6 +73,18 @@ type ActionRunJob struct {
|
||||
// MaxParallel is strategy.max-parallel, shared by all matrix jobs with the same JobID (0 = unlimited).
|
||||
MaxParallel int `xorm:"NOT NULL DEFAULT 0"`
|
||||
|
||||
// IsMatrixDeferred marks a placeholder for a job whose matrix references `needs.*.outputs.*` and so
|
||||
// could not be expanded at planning time. Its WorkflowPayload still carries the raw, unevaluated
|
||||
// matrix; the job emitter expands it once the needs finish. Only a successful expansion clears the flag:
|
||||
// it survives a terminal status (skipped, cancelled, failed expansion) so a rerun can recognize
|
||||
// the row as unexpanded and re-derive the matrix instead of dispatching the raw payload.
|
||||
IsMatrixDeferred bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
|
||||
// DeferredMatrixPayload preserves a deferred-matrix placeholder's original WorkflowPayload (the raw, unevaluated matrix).
|
||||
// A rerun whose needs re-run collapses the combinations back into a single placeholder built from this payload,
|
||||
// so the matrix is re-derived from the fresh outputs.
|
||||
DeferredMatrixPayload []byte `xorm:"LONGBLOB"`
|
||||
|
||||
// RunAttemptID identifies the ActionRunAttempt this job belongs to.
|
||||
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
|
||||
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||
@@ -323,6 +336,53 @@ func GetPriorAttemptChildrenByParent(ctx context.Context, runID, currentAttemptI
|
||||
return nil, nil //nolint:nilnil // every prior attempt skipped this caller
|
||||
}
|
||||
|
||||
// GetPriorAttemptMatrixCombos returns the most recent prior attempt's combination rows of the given
|
||||
// dynamic-matrix job, indexed by Name, so re-expansion keeps AttemptJobIDs stable across attempts.
|
||||
func GetPriorAttemptMatrixCombos(ctx context.Context, runID, currentAttemptID, parentAttemptJobID int64, jobID string) (map[string]*ActionRunJob, error) {
|
||||
// An unexpanded placeholder is not a combination, so it is skipped and the search looks further
|
||||
// back past it. Only the columns the scope check and the result need are read: the rows carry
|
||||
// two payload blobs, and every prior attempt of the job is a candidate.
|
||||
var candidates []*ActionRunJob
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("run_id = ? AND job_id = ? AND run_attempt_id < ? AND is_matrix_deferred = ?", runID, jobID, currentAttemptID, false).
|
||||
Cols("id", "name", "attempt_job_id", "run_attempt_id", "parent_job_id").
|
||||
Desc("run_attempt_id").
|
||||
Find(&candidates); err != nil {
|
||||
return nil, fmt.Errorf("find prior matrix combos: %w", err)
|
||||
}
|
||||
|
||||
// Every combination of one attempt shares a parent, so dedupe before the lookup.
|
||||
parentIDs := container.FilterSlice(candidates, func(c *ActionRunJob) (int64, bool) {
|
||||
return c.ParentJobID, c.ParentJobID > 0
|
||||
})
|
||||
parentAttemptIDByRowID := make(map[int64]int64, len(parentIDs))
|
||||
if len(parentIDs) > 0 {
|
||||
var parents []*ActionRunJob
|
||||
if err := db.GetEngine(ctx).In("id", parentIDs).Cols("id", "attempt_job_id").Find(&parents); err != nil {
|
||||
return nil, fmt.Errorf("find prior matrix combo parents: %w", err)
|
||||
}
|
||||
for _, p := range parents {
|
||||
parentAttemptIDByRowID[p.ID] = p.AttemptJobID
|
||||
}
|
||||
}
|
||||
|
||||
// Rows arrive newest-attempt-first, so the first in-scope row fixes the attempt to take.
|
||||
combos := map[string]*ActionRunJob{}
|
||||
newestAttemptID := int64(0)
|
||||
for _, c := range candidates {
|
||||
if parentAttemptIDByRowID[c.ParentJobID] != parentAttemptJobID {
|
||||
continue
|
||||
}
|
||||
if newestAttemptID == 0 {
|
||||
newestAttemptID = c.RunAttemptID
|
||||
} else if c.RunAttemptID != newestAttemptID {
|
||||
break
|
||||
}
|
||||
combos[c.Name] = c
|
||||
}
|
||||
return combos, nil
|
||||
}
|
||||
|
||||
// GetDirectChildJobsByParent returns the direct child jobs of a parent job (e.g. a reusable workflow caller).
|
||||
func GetDirectChildJobsByParent(ctx context.Context, parentJob *ActionRunJob) (ActionJobList, error) {
|
||||
var jobs []*ActionRunJob
|
||||
|
||||
Reference in New Issue
Block a user