From 64f4b5856a070931ee0d277fa74c663aaf48b348 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 5 Oct 2026 12:29:49 +0200 Subject: [PATCH] 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 Co-authored-by: bircni --- go.mod | 4 +- go.sum | 8 +-- modules/actions/jobparser/jobparser.go | 61 +++++++++---------- modules/actions/jobparser/jobparser_test.go | 42 +++++++++++++ .../js/components/WorkflowGraph.utils.test.ts | 3 +- web_src/js/components/WorkflowGraph.utils.ts | 6 +- 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index c631f54dd11..a71144ed9e4 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( connectrpc.com/connect v1.21.0 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 - gitea.dev/actionslib v1.2.1 + gitea.dev/actionslib v1.3.0 gitea.dev/sdk v1.2.0 github.com/42wim/httpsig v1.2.4 github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 @@ -135,7 +135,7 @@ require ( github.com/blevesearch/zapx/v15 v15.4.3 // indirect github.com/blevesearch/zapx/v16 v16.3.4 // indirect github.com/blevesearch/zapx/v17 v17.2.3 // indirect - github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.4 // indirect github.com/bodgit/windows v1.0.1 // indirect diff --git a/go.sum b/go.sum index a76d84ae1c0..871656b3cf6 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHq gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= -gitea.dev/actionslib v1.2.1 h1:GL//K/0zIZV6h1OOksv1awvFVg0rg7fug2xWcG7mkG0= -gitea.dev/actionslib v1.2.1/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4= +gitea.dev/actionslib v1.3.0 h1:xZRoTL1+sK/PaglT9uXkZJ9g0nk1WLMEH4FUXdXpKKw= +gitea.dev/actionslib v1.3.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ= gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo= gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE= github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= @@ -108,8 +108,8 @@ github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaT github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg= github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po= github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE= -github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= -github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw= +github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= diff --git a/modules/actions/jobparser/jobparser.go b/modules/actions/jobparser/jobparser.go index 4230903143d..f37e4c92bb6 100644 --- a/modules/actions/jobparser/jobparser.go +++ b/modules/actions/jobparser/jobparser.go @@ -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, ", ") + ")" } diff --git a/modules/actions/jobparser/jobparser_test.go b/modules/actions/jobparser/jobparser_test.go index 4606e9eb294..0a80219da6e 100644 --- a/modules/actions/jobparser/jobparser_test.go +++ b/modules/actions/jobparser/jobparser_test.go @@ -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: diff --git a/web_src/js/components/WorkflowGraph.utils.test.ts b/web_src/js/components/WorkflowGraph.utils.test.ts index a8da59b95f3..54912a695b3 100644 --- a/web_src/js/components/WorkflowGraph.utils.test.ts +++ b/web_src/js/components/WorkflowGraph.utils.test.ts @@ -59,12 +59,13 @@ const wfTest1Jobs: ActionsJob[] = [ const mockJob = (id: number, jobId: string, name: string, needs?: string[]): ActionsJob => ({id, link: '', jobId, name, status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '1s', needs}); -test('matrix nodes key on job id, not on the display name', () => { +test('matrix nodes key on job id and take the label legs share with or without a suffix', () => { const legs = createWorkflowGraphModel([mockJob(1, 'explicit', 'leg one'), mockJob(2, 'explicit', 'leg two')]); expect(legs.nodes).toHaveLength(1); expect(legs.nodes[0].type).toBe('matrix'); expect(legs.nodes[0].name).toBe('explicit'); expect(legs.nodes[0].jobs.map((j) => j.id)).toEqual([1, 2]); + expect(createWorkflowGraphModel([mockJob(1, 'image', 'Image'), mockJob(2, 'image', 'Image (dind)')]).nodes[0].name).toBe('Image'); const lookalikes = createWorkflowGraphModel([ mockJob(1, 'setup', 'setup'), diff --git a/web_src/js/components/WorkflowGraph.utils.ts b/web_src/js/components/WorkflowGraph.utils.ts index d862943a5e4..31165427a46 100644 --- a/web_src/js/components/WorkflowGraph.utils.ts +++ b/web_src/js/components/WorkflowGraph.utils.ts @@ -1,3 +1,4 @@ +import {cutString} from '../utils/string.ts'; import type {ActionsJob, ActionsStatus} from '../modules/gitea-actions.ts'; export type GraphNodeType = 'job' | 'matrix' | 'group'; @@ -89,10 +90,7 @@ function graphIdForJob(job: ActionsJob): string { // matrix legs are named ` ()`; a workflow-provided `name:` may not be function matrixLabel(matrixJobs: ActionsJob[], jobId: string): string { - const prefixes = new Set(matrixJobs.map((job) => { - const idx = job.name.indexOf(' ('); - return idx === -1 ? '' : job.name.slice(0, idx).trim(); - })); + const prefixes = new Set(matrixJobs.map((job) => cutString(job.name, ' (')[0].trim())); const [prefix] = prefixes; return prefixes.size === 1 && prefix ? prefix : jobId; }