From 187daac598ef679feca3adc0cfb1f9d2aa785789 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 13 May 2026 09:30:22 +0200 Subject: [PATCH] fix: Sort action run jobs by JobID and Name with matrix examples (#37046) Fix the sorting of jobs out of a matrix ## Before grafik ## After grafik --------- Signed-off-by: Nicolas Co-authored-by: Claude Sonnet 4.6 Co-authored-by: wxiaoguang Co-authored-by: silverwind --- models/actions/run_job_list.go | 18 +++++++++ models/actions/run_job_list_test.go | 61 +++++++++++++++++++++++++++++ routers/api/v1/repo/action.go | 1 + routers/web/repo/actions/view.go | 1 + 4 files changed, 81 insertions(+) create mode 100644 models/actions/run_job_list_test.go diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index db7554593d..50c860bf8b 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -5,9 +5,11 @@ package actions import ( "context" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" @@ -23,6 +25,22 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +// SortMatrixGroupsByName natural-sorts each contiguous run of jobs that share a JobID +// so matrix expansions (e.g. "test (1)", "test (2)", "test (10)") appear in human order. +// Input is expected to be in DB id order so JobID groups are contiguous; cross-group order is preserved. +func (jobs ActionJobList) SortMatrixGroupsByName() { + for i := 0; i < len(jobs); { + j := i + 1 + for j < len(jobs) && jobs[j].JobID == jobs[i].JobID { + j++ + } + slices.SortFunc(jobs[i:j], func(a, b *ActionRunJob) int { + return base.NaturalSortCompare(a.Name, b.Name) + }) + i = j + } +} + func (jobs ActionJobList) LoadRepos(ctx context.Context) error { repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { return j.RepoID, j.RepoID != 0 && j.Repo == nil diff --git a/models/actions/run_job_list_test.go b/models/actions/run_job_list_test.go new file mode 100644 index 0000000000..271031b4cd --- /dev/null +++ b/models/actions/run_job_list_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestActionJobList_SortMatrixGroupsByName(t *testing.T) { + mk := func(jobID, name string) *ActionRunJob { + return &ActionRunJob{JobID: jobID, Name: name} + } + names := func(jobs ActionJobList) []string { + out := make([]string, len(jobs)) + for i, j := range jobs { + out[i] = j.Name + } + return out + } + + t.Run("matrix group sorted naturally", func(t *testing.T) { + jobs := ActionJobList{ + mk("build", "build"), + mk("test", "test (10)"), + mk("test", "test (2)"), + mk("test", "test (1)"), + mk("deploy", "deploy"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"build", "test (1)", "test (2)", "test (10)", "deploy"}, names(jobs)) + }) + + t.Run("non-adjacent same JobID stays in input order", func(t *testing.T) { + jobs := ActionJobList{ + mk("test", "test (10)"), + mk("build", "build"), + mk("test", "test (1)"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"test (10)", "build", "test (1)"}, names(jobs)) + }) + + t.Run("groups stay in input order", func(t *testing.T) { + jobs := ActionJobList{ + mk("z", "z"), + mk("a", "a"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"z", "a"}, names(jobs)) + }) + + t.Run("empty and singleton", func(t *testing.T) { + ActionJobList(nil).SortMatrixGroupsByName() + jobs := ActionJobList{mk("only", "only")} + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"only"}, names(jobs)) + }) +} diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d23fc849ac..09b5ce2db5 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1175,6 +1175,7 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac ctx.APIErrorInternal(err) return nil, nil } + jobs.SortMatrixGroupsByName() return run, jobs } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 726ab63e56..99f15a4605 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -893,6 +893,7 @@ func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.A ctx.NotFound(nil) return nil, nil, nil } + jobs.SortMatrixGroupsByName() for _, job := range jobs { job.Run = run