Skip to content

Commit f720110

Browse files
silverwindclaudewxiaoguang
committed
Add "Run" prefix for unnamed action steps (go-gitea#36624)
Steps defined with `run:` or `uses:` without an explicit `name:` now display with a "Run <cmd>" prefix in the Actions log UI, matching GitHub Actions behavior. <img width="311" height="236" alt="image" src="https://github.com/user-attachments/assets/9fde83f5-c43a-4732-ac55-0f4e1fbc1314" /> --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent bbbeddf commit f720110

5 files changed

Lines changed: 145 additions & 6 deletions

File tree

models/actions/task.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/subtle"
99
"errors"
1010
"fmt"
11+
"strings"
1112
"time"
1213

1314
auth_model "code.gitea.io/gitea/models/auth"
@@ -20,6 +21,7 @@ import (
2021

2122
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
2223
lru "github.com/hashicorp/golang-lru/v2"
24+
"github.com/nektos/act/pkg/jobparser"
2325
"google.golang.org/protobuf/types/known/timestamppb"
2426
"xorm.io/builder"
2527
)
@@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
214216
return nil, errNotExist
215217
}
216218

219+
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
220+
if step.Name != "" {
221+
name = step.Name // the step has an explicit name
222+
} else {
223+
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
224+
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
225+
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
226+
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
227+
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
228+
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
229+
}
230+
return util.EllipsisDisplayString(name, limit) // database column has a length limit
231+
}
232+
217233
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
218234
ctx, committer, err := db.TxContext(ctx)
219235
if err != nil {
@@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
293309
if len(workflowJob.Steps) > 0 {
294310
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
295311
for i, v := range workflowJob.Steps {
296-
name := util.EllipsisDisplayString(v.String(), 255)
297312
steps[i] = &ActionTaskStep{
298-
Name: name,
313+
Name: makeTaskStepDisplayName(v, 255),
299314
TaskID: task.ID,
300315
Index: int64(i),
301316
RepoID: task.RepoID,

models/actions/task_step.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// ActionTaskStep represents a step of ActionTask
1515
type ActionTaskStep struct {
1616
ID int64
17-
Name string `xorm:"VARCHAR(255)"`
17+
Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long
1818
TaskID int64 `xorm:"index unique(task_index)"`
1919
Index int64 `xorm:"index unique(task_index)"`
2020
RepoID int64 `xorm:"index"`

models/actions/task_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"github.com/nektos/act/pkg/jobparser"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestMakeTaskStepDisplayName(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
jobStep *jobparser.Step
18+
expected string
19+
}{
20+
{
21+
name: "explicit name",
22+
jobStep: &jobparser.Step{
23+
Name: "Test Step",
24+
},
25+
expected: "Test Step",
26+
},
27+
{
28+
name: "uses step",
29+
jobStep: &jobparser.Step{
30+
Uses: "actions/checkout@v4",
31+
},
32+
expected: "Run actions/checkout@v4",
33+
},
34+
{
35+
name: "single-line run",
36+
jobStep: &jobparser.Step{
37+
Run: "echo hello",
38+
},
39+
expected: "Run echo hello",
40+
},
41+
{
42+
name: "multi-line run block scalar",
43+
jobStep: &jobparser.Step{
44+
Run: "\n echo hello \r\n echo world \n ",
45+
},
46+
expected: "Run echo hello",
47+
},
48+
{
49+
name: "fallback to id",
50+
jobStep: &jobparser.Step{
51+
ID: "step-id",
52+
},
53+
expected: "Run step-id",
54+
},
55+
{
56+
name: "very long name truncated",
57+
jobStep: &jobparser.Step{
58+
Name: strings.Repeat("a", 300),
59+
},
60+
expected: strings.Repeat("a", 252) + "…",
61+
},
62+
{
63+
name: "very long run truncated",
64+
jobStep: &jobparser.Step{
65+
Run: strings.Repeat("a", 300),
66+
},
67+
expected: "Run " + strings.Repeat("a", 248) + "…",
68+
},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
result := makeTaskStepDisplayName(tt.jobStep, 255)
73+
assert.Equal(t, tt.expected, result)
74+
})
75+
}
76+
}

routers/web/repo/actions/view.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"code.gitea.io/gitea/modules/log"
2828
"code.gitea.io/gitea/modules/storage"
2929
"code.gitea.io/gitea/modules/templates"
30+
"code.gitea.io/gitea/modules/translation"
3031
"code.gitea.io/gitea/modules/util"
3132
"code.gitea.io/gitea/modules/web"
3233
"code.gitea.io/gitea/routers/common"
@@ -302,7 +303,7 @@ func ViewPost(ctx *context_module.Context) {
302303
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
303304
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
304305
if task != nil {
305-
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
306+
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
306307
if err != nil {
307308
ctx.ServerError("convertToViewModel", err)
308309
return
@@ -314,7 +315,7 @@ func ViewPost(ctx *context_module.Context) {
314315
ctx.JSON(http.StatusOK, resp)
315316
}
316317

317-
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
318+
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
318319
var viewJobs []*ViewJobStep
319320
var logs []*ViewStepLog
320321

@@ -344,7 +345,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
344345
Lines: []*ViewStepLogLine{
345346
{
346347
Index: 1,
347-
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
348+
Message: locale.TrString("actions.runs.expire_log_message"),
348349
// Timestamp doesn't mean anything when the log is expired.
349350
// Set it to the task's updated time since it's probably the time when the log has expired.
350351
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"testing"
8+
9+
actions_model "code.gitea.io/gitea/models/actions"
10+
"code.gitea.io/gitea/modules/timeutil"
11+
"code.gitea.io/gitea/modules/translation"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestConvertToViewModel(t *testing.T) {
18+
task := &actions_model.ActionTask{
19+
Status: actions_model.StatusSuccess,
20+
Steps: []*actions_model.ActionTaskStep{
21+
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
22+
},
23+
Stopped: timeutil.TimeStamp(20),
24+
}
25+
26+
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
27+
require.NoError(t, err)
28+
29+
expectedViewJobs := []*ViewJobStep{
30+
{
31+
Summary: "Set up job",
32+
Duration: "0s",
33+
Status: "success",
34+
},
35+
{
36+
Summary: "Run step-name",
37+
Duration: "4s",
38+
Status: "success",
39+
},
40+
{
41+
Summary: "Complete job",
42+
Duration: "15s",
43+
Status: "success",
44+
},
45+
}
46+
assert.Equal(t, expectedViewJobs, viewJobSteps)
47+
}

0 commit comments

Comments
 (0)