mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-19 16:18:11 +00:00
enhance(actions): show the steps the runner reports (#39720)
Gitea derives the steps of a job from the workflow file, so the `pre` and `post` steps of actions only show up inside "Set up job" and "Complete job". Fixes https://github.com/go-gitea/gitea/issues/24604. A runner can now report the steps it actually runs, and Gitea shows those instead. Runners that don't report them keep the current view, and older Gitea versions ignore them. Related: - Proto: https://gitea.com/gitea/actions-proto-def/pulls/19 - Runner: https://gitea.com/gitea/runner/pulls/1293
This commit is contained in:
@@ -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.3.0
|
||||
gitea.dev/actionslib v1.4.0
|
||||
gitea.dev/sdk v1.3.0
|
||||
github.com/42wim/httpsig v1.3.0
|
||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||
|
||||
@@ -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.3.0 h1:xZRoTL1+sK/PaglT9uXkZJ9g0nk1WLMEH4FUXdXpKKw=
|
||||
gitea.dev/actionslib v1.3.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ=
|
||||
gitea.dev/actionslib v1.4.0 h1:k31smj22tnUZrrM5kkntXN8lKAe7dkoTEoiP1Fk5KJc=
|
||||
gitea.dev/actionslib v1.4.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ=
|
||||
gitea.dev/sdk v1.3.0 h1:4U95+Ou6WHCQiRPDEyAzvl1Q7cxg5RUTWkqmOXr5w4I=
|
||||
gitea.dev/sdk v1.3.0/go.mod h1:85EBqSAqUn1KlFCk40y3sTjWjBsB/KouL3slGXMA/2o=
|
||||
github.com/42wim/httpsig v1.3.0 h1:HK2bieeZ99X9IvOoc1ST6yrhLek0f4MqlEGES2JCP3g=
|
||||
|
||||
@@ -435,6 +435,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(356, "Add index on action_run commit_sha", v29.AddActionRunCommitSHAIndex),
|
||||
newMigration(357, "Normalize legacy team authorize values", v29.NormalizeLegacyTeamAuthorize),
|
||||
newMigration(358, "Track previous remember-me token hashes", v29.AddPreviousAuthTokenHash),
|
||||
newMigration(359, "Add stage and number columns to action_task_step", v29.AddStageAndNumberToActionTaskStep),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddStageAndNumberToActionTaskStep(_ context.Context, x base.EngineMigration) error {
|
||||
type ActionTaskStep struct {
|
||||
Stage int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
Number int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreDropIndices: true,
|
||||
}, new(ActionTaskStep))
|
||||
return err
|
||||
}
|
||||
+14
-22
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -482,6 +483,11 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
for _, v := range state.Steps {
|
||||
stepStates[v.Id] = v
|
||||
}
|
||||
reportedSteps := state.ReportedSteps
|
||||
if err := validateReportedSteps(reportedSteps); err != nil {
|
||||
log.Warn("Ignore the steps reported for task %d: %v", state.Id, err)
|
||||
reportedSteps = nil
|
||||
}
|
||||
|
||||
// Only one request can update the task because the final state needs to be calculated with all job states.
|
||||
// Otherwise, concurrent requests with transaction will make the SQL read stale job state and result in wrong final state.
|
||||
@@ -538,22 +544,14 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reportedSteps) > 0 {
|
||||
return updateReportedSteps(ctx, task, reportedSteps, now)
|
||||
}
|
||||
if slices.ContainsFunc(task.Steps, (*ActionTaskStep).IsReported) {
|
||||
return nil // the indexes of the steps of the workflow file no longer match the stored steps
|
||||
}
|
||||
for _, step := range task.Steps {
|
||||
var result runnerv1.Result
|
||||
if v, ok := stepStates[step.Index]; ok {
|
||||
result = v.Result
|
||||
step.LogIndex = v.LogIndex
|
||||
step.LogLength = v.LogLength
|
||||
if step.Started == 0 && v.StartedAt != nil {
|
||||
step.Started = now
|
||||
}
|
||||
}
|
||||
if result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
step.Status = StatusFromResult(result)
|
||||
step.Stopped = util.IfZero(step.Stopped, now)
|
||||
} else if step.Started != 0 {
|
||||
step.Status = StatusRunning
|
||||
}
|
||||
step.applyState(stepStates[step.Index], now)
|
||||
if _, err := db.GetEngine(ctx).ID(step.ID).Update(step); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -637,13 +635,7 @@ func StopTask(ctx context.Context, taskID int64, status Status) error {
|
||||
}
|
||||
|
||||
for _, step := range task.Steps {
|
||||
if !step.Status.IsDone() {
|
||||
step.Status = status
|
||||
if step.Started == 0 {
|
||||
step.Started = now
|
||||
}
|
||||
step.Stopped = now
|
||||
}
|
||||
step.stop(status, now)
|
||||
if _, err := e.ID(step.ID).Update(step); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// ActionTaskStep represents a step of ActionTask
|
||||
@@ -21,6 +25,8 @@ type ActionTaskStep struct {
|
||||
Status Status `xorm:"index"`
|
||||
LogIndex int64
|
||||
LogLength int64
|
||||
Stage int64 `xorm:"NOT NULL DEFAULT 0"` // a runnerv1.StepStage, unspecified for the steps parsed from the workflow file instead of reported by the runner
|
||||
Number int64 `xorm:"NOT NULL DEFAULT 0"` // the index in the workflow file of the step a PRE, MAIN or POST stage belongs to
|
||||
Started timeutil.TimeStamp
|
||||
Stopped timeutil.TimeStamp
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
@@ -31,6 +37,27 @@ func (step *ActionTaskStep) Duration() time.Duration {
|
||||
return calculateDuration(step.Started, step.Stopped, step.Status, step.Updated)
|
||||
}
|
||||
|
||||
// StepStage returns the stored stage, which can be one added to the enum after this Gitea version
|
||||
func (step *ActionTaskStep) StepStage() runnerv1.StepStage {
|
||||
return runnerv1.StepStage(step.Stage)
|
||||
}
|
||||
|
||||
// IsReported reports whether the runner reported the step instead of Gitea parsing it from the workflow file.
|
||||
func (step *ActionTaskStep) IsReported() bool {
|
||||
return step.StepStage() != runnerv1.StepStage_STEP_STAGE_UNSPECIFIED
|
||||
}
|
||||
|
||||
// WorkflowStepIndex returns the index of the step in the workflow file, or -1 for a step that is not written there.
|
||||
func (step *ActionTaskStep) WorkflowStepIndex() int64 {
|
||||
switch step.StepStage() {
|
||||
case runnerv1.StepStage_STEP_STAGE_UNSPECIFIED:
|
||||
return step.Index
|
||||
case runnerv1.StepStage_STEP_STAGE_MAIN:
|
||||
return step.Number
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionTaskStep))
|
||||
}
|
||||
@@ -39,3 +66,120 @@ func GetTaskStepsByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskStep,
|
||||
var steps []*ActionTaskStep
|
||||
return steps, db.GetEngine(ctx).Where("task_id=?", taskID).OrderBy("`index` ASC").Find(&steps)
|
||||
}
|
||||
|
||||
// applyState applies the state the runner reported for the step, nil meaning it reported none.
|
||||
func (step *ActionTaskStep) applyState(state *runnerv1.StepState, now timeutil.TimeStamp) {
|
||||
var result runnerv1.Result
|
||||
if state != nil {
|
||||
result = state.Result
|
||||
step.LogIndex = state.LogIndex
|
||||
step.LogLength = state.LogLength
|
||||
if step.Started == 0 && state.StartedAt != nil {
|
||||
step.Started = now
|
||||
}
|
||||
}
|
||||
if result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||
step.Status = StatusFromResult(result)
|
||||
step.Stopped = util.IfZero(step.Stopped, now)
|
||||
} else if step.Started != 0 {
|
||||
step.Status = StatusRunning
|
||||
}
|
||||
}
|
||||
|
||||
// stop finishes a step that had not finished when its task stopped
|
||||
func (step *ActionTaskStep) stop(status Status, now timeutil.TimeStamp) {
|
||||
if step.Status.IsDone() {
|
||||
return
|
||||
}
|
||||
step.Status = status
|
||||
step.Started = util.IfZero(step.Started, now)
|
||||
step.Stopped = now
|
||||
}
|
||||
|
||||
// maxReportedSteps bounds the steps a runner can make Gitea store for one task.
|
||||
const maxReportedSteps = 1000
|
||||
|
||||
type reportedStepKey struct {
|
||||
stage runnerv1.StepStage
|
||||
number int64
|
||||
}
|
||||
|
||||
func validateReportedSteps(steps []*runnerv1.StepState) error {
|
||||
if len(steps) > maxReportedSteps {
|
||||
return fmt.Errorf("%d steps exceed the limit of %d", len(steps), maxReportedSteps)
|
||||
}
|
||||
seen := make(container.Set[reportedStepKey], len(steps))
|
||||
for i, v := range steps {
|
||||
if v.Stage <= runnerv1.StepStage_STEP_STAGE_UNSPECIFIED { // a stage added later is shown by its name
|
||||
return fmt.Errorf("step %d has invalid stage %d", i, v.Stage)
|
||||
}
|
||||
if v.Id != int64(i) || v.Number < 0 || v.LogIndex < 0 || v.LogLength < 0 {
|
||||
return fmt.Errorf("step %d has invalid id %d, number %d or log range %d+%d", i, v.Id, v.Number, v.LogIndex, v.LogLength)
|
||||
}
|
||||
if !seen.Add(reportedStepKey{v.Stage, v.Number}) {
|
||||
return fmt.Errorf("step %d duplicates stage %s of number %d", i, v.Stage, v.Number)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateReportedSteps replaces the steps of the task with the ones the runner reported.
|
||||
// A step keeps its times and status when the runner reports it again at another position.
|
||||
func updateReportedSteps(ctx context.Context, task *ActionTask, reported []*runnerv1.StepState, now timeutil.TimeStamp) error {
|
||||
e := db.GetEngine(ctx)
|
||||
sameLayout := len(task.Steps) == len(reported)
|
||||
for i := 0; sameLayout && i < len(reported); i++ {
|
||||
step := task.Steps[i]
|
||||
sameLayout = step.IsReported() && reportedStepKey{step.StepStage(), step.Number} == reportedStepKey{reported[i].Stage, reported[i].Number}
|
||||
}
|
||||
|
||||
if sameLayout {
|
||||
for i, step := range task.Steps {
|
||||
step.Name = util.EllipsisDisplayString(reported[i].Name, 255)
|
||||
step.applyState(reported[i], now)
|
||||
if task.Status.IsDone() {
|
||||
step.stop(task.Status, now)
|
||||
}
|
||||
if _, err := e.ID(step.ID).Update(step); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
known := make(map[reportedStepKey]*ActionTaskStep, len(task.Steps))
|
||||
for _, step := range task.Steps {
|
||||
if step.IsReported() {
|
||||
known[reportedStepKey{step.StepStage(), step.Number}] = step
|
||||
}
|
||||
}
|
||||
steps := make([]*ActionTaskStep, len(reported))
|
||||
for i, v := range reported {
|
||||
key := reportedStepKey{v.Stage, v.Number}
|
||||
step := &ActionTaskStep{
|
||||
Name: util.EllipsisDisplayString(v.Name, 255),
|
||||
TaskID: task.ID,
|
||||
Index: int64(i),
|
||||
RepoID: task.RepoID,
|
||||
Stage: int64(key.stage),
|
||||
Number: key.number,
|
||||
Status: StatusWaiting,
|
||||
}
|
||||
if old, ok := known[key]; ok {
|
||||
step.Status, step.Started, step.Stopped = old.Status, old.Started, old.Stopped
|
||||
}
|
||||
step.applyState(v, now)
|
||||
if task.Status.IsDone() {
|
||||
step.stop(task.Status, now)
|
||||
}
|
||||
steps[i] = step
|
||||
}
|
||||
if _, err := e.Delete(&ActionTaskStep{TaskID: task.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := e.Insert(steps); err != nil {
|
||||
return err
|
||||
}
|
||||
task.Steps = steps
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -407,6 +407,114 @@ func TestUpdateTaskByStateIsAtomic(t *testing.T) {
|
||||
assert.GreaterOrEqual(t, unittest.AssertExistsAndLoadBean(t, &ActionTaskStep{TaskID: task.ID}).Started, before)
|
||||
}
|
||||
|
||||
func TestUpdateTaskByStateReportedSteps(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
task, _ := newRunningTaskForCancelling(t, "reported-steps-job", true)
|
||||
require.NoError(t, db.Insert(t.Context(), &ActionTaskStep{Name: "Run echo", TaskID: task.ID, RepoID: task.RepoID, Status: StatusWaiting}))
|
||||
|
||||
report := func(t *testing.T, steps []*runnerv1.StepState, reported ...*runnerv1.StepState) []*ActionTaskStep {
|
||||
t.Helper()
|
||||
_, err := UpdateTaskByState(t.Context(), task.RunnerID, &runnerv1.TaskState{Id: task.ID, Steps: steps, ReportedSteps: reported})
|
||||
require.NoError(t, err)
|
||||
got, err := GetTaskStepsByTaskID(t.Context(), task.ID)
|
||||
require.NoError(t, err)
|
||||
return got
|
||||
}
|
||||
started := timestamppb.Now()
|
||||
setup := &runnerv1.StepState{Id: 0, Name: "Set up job", Stage: runnerv1.StepStage_STEP_STAGE_SETUP, StartedAt: started}
|
||||
main := &runnerv1.StepState{Id: 1, Name: "Run echo", Stage: runnerv1.StepStage_STEP_STAGE_MAIN}
|
||||
legacy := []*runnerv1.StepState{{Id: 0, StartedAt: started, LogLength: 3}}
|
||||
|
||||
t.Run("invalid reported steps fall back to the workflow steps", func(t *testing.T) {
|
||||
got := report(t, legacy, setup, &runnerv1.StepState{Id: 1, Stage: runnerv1.StepStage_STEP_STAGE_SETUP})
|
||||
require.Len(t, got, 1)
|
||||
assert.False(t, got[0].IsReported())
|
||||
assert.Equal(t, StatusRunning, got[0].Status)
|
||||
assert.EqualValues(t, 3, got[0].LogLength)
|
||||
})
|
||||
|
||||
t.Run("reported steps replace the workflow steps", func(t *testing.T) {
|
||||
got := report(t, legacy, setup, main)
|
||||
require.Len(t, got, 2)
|
||||
assert.Equal(t, runnerv1.StepStage_STEP_STAGE_SETUP, got[0].StepStage())
|
||||
assert.EqualValues(t, -1, got[0].WorkflowStepIndex())
|
||||
assert.Equal(t, StatusRunning, got[0].Status)
|
||||
assert.NotZero(t, got[0].Started)
|
||||
assert.Equal(t, "Run echo", got[1].Name)
|
||||
assert.Equal(t, StatusWaiting, got[1].Status)
|
||||
assert.EqualValues(t, 0, got[1].WorkflowStepIndex())
|
||||
})
|
||||
|
||||
t.Run("an inserted step keeps the state of the others", func(t *testing.T) {
|
||||
before, err := GetTaskStepsByTaskID(t.Context(), task.ID)
|
||||
require.NoError(t, err)
|
||||
doneSetup := &runnerv1.StepState{Id: 0, Name: "Set up job", Stage: runnerv1.StepStage_STEP_STAGE_SETUP, StartedAt: started, Result: runnerv1.Result_RESULT_SUCCESS, LogLength: 4}
|
||||
pre := &runnerv1.StepState{Id: 1, Name: "Pre Run checkout", Stage: runnerv1.StepStage_STEP_STAGE_PRE, StartedAt: started, LogIndex: 4, LogLength: 2}
|
||||
movedMain := &runnerv1.StepState{Id: 2, Name: "Run echo", Stage: runnerv1.StepStage_STEP_STAGE_MAIN}
|
||||
got := report(t, legacy, doneSetup, pre, movedMain)
|
||||
require.Len(t, got, 3)
|
||||
assert.Equal(t, StatusSuccess, got[0].Status)
|
||||
assert.Equal(t, before[0].Started, got[0].Started)
|
||||
assert.Equal(t, runnerv1.StepStage_STEP_STAGE_PRE, got[1].StepStage())
|
||||
assert.Equal(t, StatusRunning, got[1].Status)
|
||||
assert.EqualValues(t, 4, got[1].LogIndex)
|
||||
assert.EqualValues(t, 2, got[2].Index)
|
||||
})
|
||||
|
||||
t.Run("workflow steps no longer apply once reported", func(t *testing.T) {
|
||||
before, err := GetTaskStepsByTaskID(t.Context(), task.ID)
|
||||
require.NoError(t, err)
|
||||
got := report(t, []*runnerv1.StepState{{Id: 0, Result: runnerv1.Result_RESULT_FAILURE}})
|
||||
assert.Equal(t, before, got)
|
||||
})
|
||||
|
||||
t.Run("unfinished steps stop with the task", func(t *testing.T) {
|
||||
doneSetup := &runnerv1.StepState{Id: 0, Name: "Set up job", Stage: runnerv1.StepStage_STEP_STAGE_SETUP, StartedAt: started, Result: runnerv1.Result_RESULT_SUCCESS}
|
||||
runningMain := &runnerv1.StepState{Id: 1, Name: "Run echo", Stage: runnerv1.StepStage_STEP_STAGE_MAIN, StartedAt: started}
|
||||
cleanup := &runnerv1.StepState{Id: 2, Name: "Complete job", Stage: runnerv1.StepStage_STEP_STAGE_CLEANUP}
|
||||
_, err := UpdateTaskByState(t.Context(), task.RunnerID, &runnerv1.TaskState{
|
||||
Id: task.ID, Result: runnerv1.Result_RESULT_FAILURE,
|
||||
ReportedSteps: []*runnerv1.StepState{doneSetup, runningMain, cleanup},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
got, err := GetTaskStepsByTaskID(t.Context(), task.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
assert.Equal(t, StatusSuccess, got[0].Status)
|
||||
for _, step := range got[1:] {
|
||||
assert.Equal(t, StatusFailure, step.Status, step.Name)
|
||||
assert.NotZero(t, step.Started, step.Name)
|
||||
assert.NotZero(t, step.Stopped, step.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateReportedSteps(t *testing.T) {
|
||||
step := func(id int64, stage runnerv1.StepStage, number int64) *runnerv1.StepState {
|
||||
return &runnerv1.StepState{Id: id, Stage: stage, Number: number}
|
||||
}
|
||||
assert.NoError(t, validateReportedSteps([]*runnerv1.StepState{
|
||||
step(0, runnerv1.StepStage_STEP_STAGE_SETUP, 0),
|
||||
step(1, runnerv1.StepStage_STEP_STAGE_PRE, 0),
|
||||
step(2, runnerv1.StepStage_STEP_STAGE_MAIN, 0),
|
||||
step(3, runnerv1.StepStage_STEP_STAGE_MAIN, 1),
|
||||
step(4, runnerv1.StepStage_STEP_STAGE_CLEANUP, 0),
|
||||
step(5, runnerv1.StepStage(100), 0), // a stage added later, with several steps told apart by number
|
||||
step(6, runnerv1.StepStage(100), 1),
|
||||
}))
|
||||
for name, steps := range map[string][]*runnerv1.StepState{
|
||||
"unspecified stage": {step(0, runnerv1.StepStage_STEP_STAGE_UNSPECIFIED, 0)},
|
||||
"negative stage": {step(0, runnerv1.StepStage(-1), 0)},
|
||||
"id not position": {step(1, runnerv1.StepStage_STEP_STAGE_SETUP, 0)},
|
||||
"negative number": {step(0, runnerv1.StepStage_STEP_STAGE_MAIN, -1)},
|
||||
"duplicate main": {step(0, runnerv1.StepStage_STEP_STAGE_MAIN, 0), step(1, runnerv1.StepStage_STEP_STAGE_MAIN, 0)},
|
||||
"duplicate setup": {step(0, runnerv1.StepStage_STEP_STAGE_SETUP, 0), step(1, runnerv1.StepStage_STEP_STAGE_SETUP, 0)},
|
||||
"too many": make([]*runnerv1.StepState, maxReportedSteps+1),
|
||||
} {
|
||||
assert.Error(t, validateReportedSteps(steps), name)
|
||||
}
|
||||
}
|
||||
|
||||
// newRunningTaskForCancelling inserts a running run/job/task assigned to a fresh runner,
|
||||
// which is the state every cancellation test starts from.
|
||||
func newRunningTaskForCancelling(t *testing.T, name string, hasCancellingSupport bool) (*ActionTask, *ActionRunJob) {
|
||||
|
||||
@@ -17,6 +17,9 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
if len(task.Steps) == 0 {
|
||||
return fullStepsOfEmptySteps(task)
|
||||
}
|
||||
if task.Steps[0].IsReported() {
|
||||
return task.Steps // the runner reports its own setup and cleanup steps
|
||||
}
|
||||
|
||||
// firstStep is the first step that has run or running, not include preStep.
|
||||
// For example,
|
||||
|
||||
@@ -6,6 +6,7 @@ package actions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -55,6 +56,21 @@ func TestFullSteps(t *testing.T) {
|
||||
{Name: postStepName, Status: actions_model.StatusFailure, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reported by the runner",
|
||||
task: &actions_model.ActionTask{
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Name: "Set up job", Stage: int64(runnerv1.StepStage_STEP_STAGE_SETUP), Status: actions_model.StatusSuccess, LogLength: 10},
|
||||
{Name: "Run echo", Stage: int64(runnerv1.StepStage_STEP_STAGE_MAIN), Status: actions_model.StatusRunning, LogIndex: 10, LogLength: 5},
|
||||
},
|
||||
Status: actions_model.StatusRunning,
|
||||
LogLength: 15,
|
||||
},
|
||||
want: []*actions_model.ActionTaskStep{
|
||||
{Name: "Set up job", Stage: int64(runnerv1.StepStage_STEP_STAGE_SETUP), Status: actions_model.StatusSuccess, LogLength: 10},
|
||||
{Name: "Run echo", Stage: int64(runnerv1.StepStage_STEP_STAGE_MAIN), Status: actions_model.StatusRunning, LogIndex: 10, LogLength: 5},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "first step is running",
|
||||
task: &actions_model.ActionTask{
|
||||
|
||||
@@ -41,7 +41,7 @@ func uploadJobSummary(ctx *ArtifactContext) {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "Error getting task steps")
|
||||
return
|
||||
}
|
||||
if !slices.ContainsFunc(steps, func(s *actions_model.ActionTaskStep) bool { return s.Index == stepIndex }) {
|
||||
if !slices.ContainsFunc(steps, func(s *actions_model.ActionTaskStep) bool { return s.WorkflowStepIndex() == stepIndex }) {
|
||||
ctx.HTTPError(http.StatusBadRequest, "step_index mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -334,8 +334,12 @@ async function loadJob() {
|
||||
store.viewData.runArtifacts = runJobResp.artifacts || [];
|
||||
store.viewData.currentRun = runJobResp.state.run;
|
||||
|
||||
const previousSteps = currentJob.value.steps;
|
||||
currentJob.value = runJobResp.state.currentJob;
|
||||
const jobLogs = runJobResp.logs.stepsLog ?? [];
|
||||
// a runner reporting its own steps can change them while the job runs, which moves the logs of each step position
|
||||
const stepsChanged = previousSteps.length > 0 && !isSameSteps(previousSteps, currentJob.value.steps);
|
||||
if (stepsChanged) resetStepsLogs(previousSteps);
|
||||
const jobLogs = stepsChanged ? [] : runJobResp.logs.stepsLog ?? [];
|
||||
|
||||
// sync the currentJobStepsStates to store the job step states
|
||||
for (let i = 0; i < currentJob.value.steps.length; i++) {
|
||||
@@ -381,8 +385,8 @@ async function loadJob() {
|
||||
lastLogElem.scrollIntoView({behavior: 'smooth', block: 'end'});
|
||||
}
|
||||
|
||||
// clear the interval timer if the job is done
|
||||
if (run.value.done && intervalID) {
|
||||
// clear the interval timer if the job is done, the logs of changed steps still need to be loaded
|
||||
if (run.value.done && intervalID && !stepsChanged) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
@@ -395,6 +399,21 @@ async function loadJob() {
|
||||
}
|
||||
}
|
||||
|
||||
function isSameSteps(a: Array<Step>, b: Array<Step>) {
|
||||
return a.length === b.length && a.every((step, idx) => step.summary === b[idx].summary);
|
||||
}
|
||||
|
||||
function resetStepsLogs(previousSteps: Array<Step>) {
|
||||
const expandedSummaries = new Set(previousSteps.filter((_, idx) => currentJobStepsStates.value[idx].expanded).map((step) => step.summary));
|
||||
currentJobStepsStates.value = currentJob.value.steps.map((step) => ({cursor: null, expanded: expandedSummaries.has(step.summary), manuallyCollapsed: false}));
|
||||
stepAnsiRenderers.length = 0;
|
||||
for (const el of jobStepLogs.value) {
|
||||
if (!el) continue;
|
||||
el.replaceChildren();
|
||||
el._stepLogsActiveContainer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isDone(status: ActionsStatus) {
|
||||
return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user