enhance(actions): improve matrix job titles, bump actionslib (#39485)

Bump actionslib to v1.3.0 and format matrix job titles like GitHub.

- Matrix values are listed in declaration order, nested arrays and
objects are flattened, null and empty values are skipped
- Expression-evaluated names are trimmed and fall back to the job ID
when blank
- Matrix `include` and `exclude` match keys case-insensitively and
coerce numbers like GitHub

| Matrix | Before | After |
|---|---|---|
| `v: ["a,b"]` | `job (a,b)` | `job (a,b)` |
| `v: ["a, b"]` | `job (a, b)` | `job (a, b)` |
| `v: [[a, b]]` | `job ([a b])` | `job (a, b)` |
| `os: [x], arch: [y]` | `job (y, x)` | `job (x, y)` |
| `v: [{t: a, p: "b,c"}]` | `job (map[p:b,c t:a])` | `job (a, b,c)` |
| `v: [{t: a, p: [b, c]}]` | `job (map[p:[b c] t:a])` | `job (a, b, c)`
|

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
silverwind
2026-10-05 12:29:49 +02:00
committed by GitHub
parent 27d876e311
commit 64f4b5856a
6 changed files with 81 additions and 43 deletions
+29 -32
View File
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"slices"
"sort"
"strings"
"gitea.dev/actionslib/pkg/expreval"
@@ -128,7 +127,7 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
}
}
// Keep accepting empty exclude mappings for workflow compatibility, although GitHub rejects them.
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
if err != nil {
return nil, fmt.Errorf("getMatrixes: %w", err)
}
@@ -169,7 +168,7 @@ func ExpandMatrixWithNeeds(jobID string, job *Job, gitCtx *model.GithubContext,
if err := job.Strategy.resolve(expreval.New(NewInterpeter(jobID, nil, nil, gitCtx, results, vars, inputs).Evaluate)); err != nil {
return nil, err
}
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
if err != nil {
return nil, fmt.Errorf("getMatrixes: %w", err)
}
@@ -224,33 +223,24 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
// strategy and interpolating the name, runs-on and continue-on-error with it.
func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
func buildMatrixCombos(jobID string, src *Job, matrixes []model.MatrixCombination, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
for index, matrix := range matrixes {
order[index], names[index] = index, matrixName(matrix)
order[index], names[index] = index, matrixName(matrix.NameValues)
}
slices.SortStableFunc(order, func(a, b int) int { return strings.Compare(names[a], names[b]) })
combos := make([]*Job, 0, len(matrixes))
var err error
for _, index := range order {
matrix := matrixes[index]
matrix := matrixes[index].Values
combo := src.Clone()
if combo.Name == "" {
combo.Name = jobID
}
combo.Strategy.RawMatrix = encodeMatrix(matrix)
replaceScalars(&combo.Strategy.RawMatrix, escapeExpressions)
if src.Strategy.RawMatrix.Kind != 0 {
combo.Strategy.JobIndex, combo.Strategy.JobTotal = index, len(matrixes)
}
evaluator := expreval.New(NewInterpeter(jobID, &combo.Strategy, matrix, gitCtx, results, vars, inputs).Evaluate)
if len(matrix) == 0 && gitCtx != nil {
combo.Name, err = evaluator.Interpolate(combo.Name)
combo.Name = escapeExpressions(combo.Name)
} else {
combo.Name, err = nameWithMatrix(combo.Name, matrix, evaluator)
}
if err != nil {
if combo.Name, err = jobName(combo.Name, jobID, names[index], evaluator, len(matrix) > 0 || gitCtx != nil); err != nil {
return nil, fmt.Errorf("interpolate name for job %q: %w", jobID, err)
}
if gitCtx != nil { // callers without one don't read runs-on
@@ -324,29 +314,36 @@ func encodeMatrix(matrix map[string]any) yaml.Node {
return node
}
func nameWithMatrix(name string, m map[string]any, evaluator expreval.Evaluator) (string, error) {
if len(m) == 0 {
// jobName trims names, gives plain text and lone string literals the suffix, and blank names the job ID
func jobName(name, jobID, suffix string, evaluator expreval.Evaluator, evaluate bool) (string, error) {
name = strings.TrimSpace(name)
if literal, ok := expreval.Literal(name); ok {
if literal == "" {
literal = jobID
}
return escapeExpressions(literal + suffix), nil
}
if !evaluate {
return name, nil
}
if !strings.Contains(name, "${{") || !strings.Contains(name, "}}") {
return escapeExpressions(name + " " + matrixName(m)), nil
}
name, err := evaluator.Interpolate(name)
if name = strings.TrimSpace(name); name == "" {
name = jobID
}
return escapeExpressions(name), err
}
func matrixName(m map[string]any) string {
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
// matrixName formats the name suffix, skipping null and empty values
func matrixName(values []any) string {
var names []string
for _, value := range values {
if name := exprparser.CoerceToString(value); name != "" {
names = append(names, name)
}
}
sort.Strings(ks)
vs := make([]string, 0, len(m))
for _, v := range ks {
vs = append(vs, fmt.Sprint(m[v]))
if len(names) == 0 {
return ""
}
return fmt.Sprintf("(%s)", strings.Join(vs, ", "))
return " (" + strings.Join(names, ", ") + ")"
}
@@ -312,6 +312,48 @@ func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T)
}
}
func TestParseJobNames(t *testing.T) {
result, err := Parse([]byte(`on: push
jobs:
scalars:
strategy: {matrix: {value: [[1.0, true, false, 0, 1000000000000000, '', null, {os: linux}], ['', null]]}}
steps: [{run: echo}]
trimmed:
name: ' Trimmed '
strategy: {matrix: {v: [a]}}
steps: [{run: echo}]
blank:
name: ' '
steps: [{run: echo}]
folded:
name: "${{ ' Folded' }}"
strategy: {matrix: {v: [a]}}
steps: [{run: echo}]
computed:
name: ${{ format(' {0} ', matrix.missing) }}
strategy: {matrix: {v: [a]}}
steps: [{run: echo}]
padded:
name: ${{ format(' {0} ', matrix.v) }}
strategy: {matrix: {v: [a]}}
steps: [{run: echo}]
`), WithGitContext(&model.GithubContext{}))
require.NoError(t, err)
names := map[string][]string{}
for _, parsed := range result {
id, job := parsed.Job()
names[id] = append(names[id], job.DisplayName())
}
assert.Equal(t, map[string][]string{
"scalars": {"scalars", "scalars (1, true, false, 0, 1E+15, linux)"},
"trimmed": {"Trimmed (a)"},
"blank": {"blank"},
"folded": {" Folded (a)"},
"computed": {"computed"},
"padded": {"a"},
}, names)
}
func TestJobFieldsWithoutMatrix(t *testing.T) {
const workflow = `on: push
jobs: