mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-17 05:53:51 +02:00
Add "Run" prefix for unnamed action steps (#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>
This commit is contained in:
parent
63266ba036
commit
e79112170c
@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
|
|||||||
return nil, errNotExist
|
return nil, errNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||||
|
if step.Name != "" {
|
||||||
|
name = step.Name // the step has an explicit name
|
||||||
|
} else {
|
||||||
|
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
|
||||||
|
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
|
||||||
|
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
|
||||||
|
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
|
||||||
|
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
|
||||||
|
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
|
||||||
|
}
|
||||||
|
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
||||||
|
}
|
||||||
|
|
||||||
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if len(workflowJob.Steps) > 0 {
|
if len(workflowJob.Steps) > 0 {
|
||||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||||
for i, v := range workflowJob.Steps {
|
for i, v := range workflowJob.Steps {
|
||||||
name := util.EllipsisDisplayString(v.String(), 255)
|
|
||||||
steps[i] = &ActionTaskStep{
|
steps[i] = &ActionTaskStep{
|
||||||
Name: name,
|
Name: makeTaskStepDisplayName(v, 255),
|
||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
Index: int64(i),
|
Index: int64(i),
|
||||||
RepoID: task.RepoID,
|
RepoID: task.RepoID,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
// ActionTaskStep represents a step of ActionTask
|
// ActionTaskStep represents a step of ActionTask
|
||||||
type ActionTaskStep struct {
|
type ActionTaskStep struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string `xorm:"VARCHAR(255)"`
|
Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long
|
||||||
TaskID int64 `xorm:"index unique(task_index)"`
|
TaskID int64 `xorm:"index unique(task_index)"`
|
||||||
Index int64 `xorm:"index unique(task_index)"`
|
Index int64 `xorm:"index unique(task_index)"`
|
||||||
RepoID int64 `xorm:"index"`
|
RepoID int64 `xorm:"index"`
|
||||||
|
|||||||
76
models/actions/task_test.go
Normal file
76
models/actions/task_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMakeTaskStepDisplayName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jobStep *jobparser.Step
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "explicit name",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Name: "Test Step",
|
||||||
|
},
|
||||||
|
expected: "Test Step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uses step",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Uses: "actions/checkout@v4",
|
||||||
|
},
|
||||||
|
expected: "Run actions/checkout@v4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single-line run",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Run: "echo hello",
|
||||||
|
},
|
||||||
|
expected: "Run echo hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi-line run block scalar",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Run: "\n echo hello \r\n echo world \n ",
|
||||||
|
},
|
||||||
|
expected: "Run echo hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fallback to id",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
ID: "step-id",
|
||||||
|
},
|
||||||
|
expected: "Run step-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "very long name truncated",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Name: strings.Repeat("a", 300),
|
||||||
|
},
|
||||||
|
expected: strings.Repeat("a", 252) + "…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "very long run truncated",
|
||||||
|
jobStep: &jobparser.Step{
|
||||||
|
Run: strings.Repeat("a", 300),
|
||||||
|
},
|
||||||
|
expected: "Run " + strings.Repeat("a", 248) + "…",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := makeTaskStepDisplayName(tt.jobStep, 255)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
@ -302,7 +303,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
|
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
|
||||||
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
|
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
|
||||||
if task != nil {
|
if task != nil {
|
||||||
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
|
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("convertToViewModel", err)
|
ctx.ServerError("convertToViewModel", err)
|
||||||
return
|
return
|
||||||
@ -314,7 +315,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||||
var viewJobs []*ViewJobStep
|
var viewJobs []*ViewJobStep
|
||||||
var logs []*ViewStepLog
|
var logs []*ViewStepLog
|
||||||
|
|
||||||
@ -344,7 +345,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
|
|||||||
Lines: []*ViewStepLogLine{
|
Lines: []*ViewStepLogLine{
|
||||||
{
|
{
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
|
Message: locale.TrString("actions.runs.expire_log_message"),
|
||||||
// Timestamp doesn't mean anything when the log is expired.
|
// Timestamp doesn't mean anything when the log is expired.
|
||||||
// Set it to the task's updated time since it's probably the time when the log has expired.
|
// Set it to the task's updated time since it's probably the time when the log has expired.
|
||||||
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
|||||||
47
routers/web/repo/actions/view_test.go
Normal file
47
routers/web/repo/actions/view_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertToViewModel(t *testing.T) {
|
||||||
|
task := &actions_model.ActionTask{
|
||||||
|
Status: actions_model.StatusSuccess,
|
||||||
|
Steps: []*actions_model.ActionTaskStep{
|
||||||
|
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
|
||||||
|
},
|
||||||
|
Stopped: timeutil.TimeStamp(20),
|
||||||
|
}
|
||||||
|
|
||||||
|
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedViewJobs := []*ViewJobStep{
|
||||||
|
{
|
||||||
|
Summary: "Set up job",
|
||||||
|
Duration: "0s",
|
||||||
|
Status: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Summary: "Run step-name",
|
||||||
|
Duration: "4s",
|
||||||
|
Status: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Summary: "Complete job",
|
||||||
|
Duration: "15s",
|
||||||
|
Status: "success",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user