mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-02 13:44:16 +09:00
Repository-level and instance-level runners still picked up waiting jobs of repositories with Actions disabled. https://github.com/go-gitea/gitea/pull/30331 only covered owner-level runners, by filtering them out in SQL. Pickup now checks the Actions unit of each job it is about to claim, for every runner scope, and cancels the job instead when Actions is disabled, also honoring `DISABLED_REPO_UNITS`. Disabling Actions therefore cancels its queued jobs once a matching runner polls, instead of leaving them waiting until `ABANDONED_JOB_TIMEOUT`.
553 lines
21 KiB
Go
553 lines
21 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
"gitea.dev/models/unittest"
|
|
"gitea.dev/modules/actions/jobparser"
|
|
"gitea.dev/modules/timeutil"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
"xorm.io/xorm/contexts"
|
|
)
|
|
|
|
func TestActionTask_GetRunJobLink(t *testing.T) {
|
|
repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"}
|
|
run := &ActionRun{ID: 10, Repo: repo}
|
|
job := &ActionRunJob{ID: 42, Run: run}
|
|
|
|
// a task with a loaded job links to that specific job, not just the run
|
|
task := &ActionTask{Job: job}
|
|
assert.Equal(t, run.Link()+"/jobs/42", task.GetRunJobLink())
|
|
|
|
// missing job, run or repo yields an empty link instead of a broken URL
|
|
assert.Empty(t, (&ActionTask{}).GetRunJobLink())
|
|
assert.Empty(t, (&ActionTask{Job: &ActionRunJob{ID: 42}}).GetRunJobLink())
|
|
assert.Empty(t, (&ActionTask{Job: &ActionRunJob{ID: 42, Run: &ActionRun{ID: 10}}}).GetRunJobLink())
|
|
}
|
|
|
|
func TestGetTaskRunnerNames(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
ctx := t.Context()
|
|
runner := &ActionRunner{Name: "queue-runner"}
|
|
require.NoError(t, db.Insert(ctx, runner))
|
|
task := &ActionTask{RunnerID: runner.ID, TokenHash: "queue-test-task"}
|
|
require.NoError(t, db.Insert(ctx, task))
|
|
names, err := GetTaskRunnerNames(ctx, []int64{task.ID, 987654321})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[int64]string{task.ID: runner.Name}, names)
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskCancellingFinalizesToCancelled(t *testing.T) {
|
|
testResult := func(t *testing.T, result runnerv1.Result) {
|
|
t.Helper()
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
task, job := newRunningTaskForCancelling(t, "cancelling-finalization-job", true)
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
|
|
taskAfterStop := unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID})
|
|
assert.Equal(t, StatusCancelling, taskAfterStop.Status)
|
|
|
|
updatedTask, err := UpdateTaskByState(t.Context(), task.RunnerID, &runnerv1.TaskState{
|
|
Id: task.ID,
|
|
Result: result,
|
|
StoppedAt: timestamppb.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusCancelled, updatedTask.Status)
|
|
|
|
taskAfterUpdate := unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID})
|
|
assert.Equal(t, StatusCancelled, taskAfterUpdate.Status)
|
|
|
|
jobAfterUpdate := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
|
assert.Equal(t, StatusCancelled, jobAfterUpdate.Status)
|
|
}
|
|
|
|
t.Run("runner reports success", func(t *testing.T) {
|
|
testResult(t, runnerv1.Result_RESULT_SUCCESS)
|
|
})
|
|
|
|
t.Run("runner reports failure", func(t *testing.T) {
|
|
testResult(t, runnerv1.Result_RESULT_FAILURE)
|
|
})
|
|
}
|
|
|
|
// TestStopTaskCancellingFallsBackToCancelled covers the cases where the cancelling handshake can
|
|
// never complete, so StopTask must cancel right away instead of waiting for the zombie task cleanup.
|
|
func TestStopTaskCancellingFallsBackToCancelled(t *testing.T) {
|
|
assertCancelled := func(t *testing.T, task *ActionTask, job *ActionRunJob) {
|
|
t.Helper()
|
|
|
|
taskAfterStop := unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID})
|
|
assert.Equal(t, StatusCancelled, taskAfterStop.Status)
|
|
assert.NotZero(t, taskAfterStop.Stopped)
|
|
|
|
jobAfterStop := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
|
assert.Equal(t, StatusCancelled, jobAfterStop.Status)
|
|
assert.NotZero(t, jobAfterStop.Stopped)
|
|
}
|
|
|
|
// A runner too old to know the cancelling state can only be stopped by a final status.
|
|
t.Run("legacy runner", func(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
task, job := newRunningTaskForCancelling(t, "legacy-cancelling-job", false)
|
|
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
assertCancelled(t, task, job)
|
|
})
|
|
|
|
// The runner is gone, e.g. an ephemeral runner was cleaned up, so nobody can acknowledge.
|
|
t.Run("missing runner", func(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
task, job := newRunningTaskForCancelling(t, "missing-runner-cancelling-job", true)
|
|
|
|
_, err := db.DeleteByID[ActionRunner](t.Context(), task.RunnerID)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
assertCancelled(t, task, job)
|
|
})
|
|
|
|
// The runner went silent, e.g. it gave up while Gitea was restarting, so it never picks up the request.
|
|
t.Run("silent runner", func(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
task, job := newRunningTaskForCancelling(t, "silent-runner-cancelling-job", true)
|
|
|
|
// NoAutoTime because the point of the test is an "updated" older than xorm would write
|
|
task.Updated = timeutil.TimeStampNow().AddDuration(-2 * taskReportTimeout)
|
|
_, err := db.GetEngine(t.Context()).ID(task.ID).Cols("updated").NoAutoTime().Update(task)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
assertCancelled(t, task, job)
|
|
|
|
// A runner coming back still learns the outcome from the UpdateTask response,
|
|
// and its late result does not overwrite the cancellation.
|
|
late, err := UpdateTaskByState(t.Context(), task.RunnerID, &runnerv1.TaskState{
|
|
Id: task.ID,
|
|
Result: runnerv1.Result_RESULT_SUCCESS,
|
|
StoppedAt: timestamppb.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusCancelled, late.Status)
|
|
assert.Equal(t, runnerv1.Result_RESULT_CANCELLED, late.Status.AsResult())
|
|
})
|
|
}
|
|
|
|
// TestStopTaskCancellingKeepsReportTime makes sure persisting the cancelling status does not refresh
|
|
// "updated": re-cancelling would otherwise defer both the fallback to cancelled and the zombie task
|
|
// cleanup, no matter how long the runner has been silent.
|
|
func TestStopTaskCancellingKeepsReportTime(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
task, _ := newRunningTaskForCancelling(t, "repeated-cancelling-job", true)
|
|
|
|
// NoAutoTime because the point of the test is an "updated" older than xorm would write
|
|
lastReport := timeutil.TimeStampNow().AddDuration(-taskReportTimeout / 2)
|
|
task.Updated = lastReport
|
|
_, err := db.GetEngine(t.Context()).ID(task.ID).Cols("updated").NoAutoTime().Update(task)
|
|
require.NoError(t, err)
|
|
|
|
// the runner reported recently enough, so the task waits for it to acknowledge
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
taskAfterStop := unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID})
|
|
assert.Equal(t, StatusCancelling, taskAfterStop.Status)
|
|
assert.Equal(t, lastReport, taskAfterStop.Updated)
|
|
|
|
// cancelling it again does not reset the clock either
|
|
require.NoError(t, StopTask(t.Context(), task.ID, StatusCancelling))
|
|
taskAfterSecondStop := unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID})
|
|
assert.Equal(t, StatusCancelling, taskAfterSecondStop.Status)
|
|
assert.Equal(t, lastReport, taskAfterSecondStop.Updated)
|
|
}
|
|
|
|
// TestReleaseTaskForRunner verifies that releasing a freshly-claimed task returns
|
|
// its job to the waiting queue and deletes the task and its steps, so a failure
|
|
// while assembling the runner response cannot strand the job in running state.
|
|
func TestReleaseTaskForRunner(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
runTitle string
|
|
runIndex int64
|
|
jobName string
|
|
batchSize int
|
|
}{
|
|
{"release", "release-task-test-run", 9902, "release-job", 0},
|
|
{"pagination", "pagination-test-run", 9903, "target-job", 2},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
if tt.batchSize != 0 {
|
|
defer func(orig int) { pickTaskBatchSize = orig }(pickTaskBatchSize)
|
|
pickTaskBatchSize = tt.batchSize
|
|
}
|
|
|
|
run := &ActionRun{
|
|
Title: tt.runTitle,
|
|
RepoID: 1,
|
|
OwnerID: 2,
|
|
WorkflowID: "test.yaml",
|
|
Index: tt.runIndex,
|
|
TriggerUserID: 2,
|
|
Ref: "refs/heads/main",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
Event: "push",
|
|
TriggerEvent: "push",
|
|
Status: StatusWaiting,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), run))
|
|
|
|
disabledRun := &ActionRun{RepoID: 2, TriggerUserID: 2}
|
|
require.NoError(t, db.Insert(t.Context(), disabledRun))
|
|
disabledJob := &ActionRunJob{RunID: disabledRun.ID, RepoID: 2, Status: StatusWaiting, RunsOn: []string{"ubuntu-latest"}}
|
|
require.NoError(t, db.Insert(t.Context(), disabledJob))
|
|
|
|
if tt.batchSize != 0 {
|
|
// Five waiting jobs the runner cannot run, then one it can.
|
|
for i := range 5 {
|
|
mismatch := &ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: "mismatch-" + string(rune('a'+i)),
|
|
Attempt: 1,
|
|
JobID: "mismatch-" + string(rune('a'+i)),
|
|
Status: StatusWaiting,
|
|
RunsOn: []string{"windows-latest"},
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), mismatch))
|
|
}
|
|
}
|
|
|
|
job := &ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: tt.jobName,
|
|
Attempt: 1,
|
|
JobID: tt.jobName,
|
|
Status: StatusWaiting,
|
|
RunsOn: []string{"ubuntu-latest"},
|
|
WorkflowPayload: []byte("on: push\njobs:\n " + tt.jobName + ":\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), job))
|
|
|
|
runner := &ActionRunner{
|
|
UUID: tt.name + "-runner-uuid",
|
|
Name: tt.name + "-runner",
|
|
AgentLabels: []string{"ubuntu-latest"},
|
|
}
|
|
runner.GenerateAndFillToken()
|
|
require.NoError(t, db.Insert(t.Context(), runner))
|
|
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{RepoID: run.RepoID, Type: unit.TypeActions}))
|
|
|
|
task, cancelledJobs, err := CreateTaskForRunner(t.Context(), runner)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, task)
|
|
require.Len(t, cancelledJobs, 1)
|
|
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: disabledJob.ID}).Status)
|
|
|
|
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
|
if tt.batchSize != 0 {
|
|
assert.Equal(t, StatusRunning, claimed.Status)
|
|
assert.Equal(t, task.ID, claimed.TaskID)
|
|
} else {
|
|
require.Equal(t, StatusRunning, claimed.Status)
|
|
require.Equal(t, task.ID, claimed.TaskID)
|
|
|
|
require.NoError(t, ReleaseTaskForRunner(t.Context(), task))
|
|
|
|
// Job is back in the waiting queue with no task assigned.
|
|
released := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
|
assert.Equal(t, StatusWaiting, released.Status)
|
|
assert.Zero(t, released.TaskID)
|
|
assert.Zero(t, released.Started)
|
|
|
|
// The task and its steps are gone.
|
|
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
|
|
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type failFirstStepWrite struct{ fired atomic.Bool }
|
|
|
|
func (h *failFirstStepWrite) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
|
if !h.fired.Load() && strings.HasPrefix(c.SQL, "UPDATE") && strings.Contains(c.SQL, "action_task_step") {
|
|
h.fired.Store(true)
|
|
return nil, errors.New("interrupted")
|
|
}
|
|
return c.Ctx, nil
|
|
}
|
|
|
|
func (*failFirstStepWrite) AfterProcess(*contexts.ContextHook) error { return nil }
|
|
|
|
// TestUpdateTaskByStateIsAtomic checks that an interrupted report writes nothing: a surviving task or
|
|
// job write would hit the "state is final" early return, which no retry or cleanup repairs.
|
|
func TestUpdateTaskByStateIsAtomic(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
task, job := newRunningTaskForCancelling(t, "atomic-report-job", true)
|
|
require.NoError(t, db.Insert(t.Context(), &ActionTaskStep{TaskID: task.ID, RepoID: task.RepoID, Status: StatusRunning}))
|
|
unittest.GetXORMEngine().AddHook(&failFirstStepWrite{})
|
|
skewed := ×tamppb.Timestamp{Seconds: 1}
|
|
finalState := &runnerv1.TaskState{Id: task.ID, Result: runnerv1.Result_RESULT_SUCCESS, StoppedAt: skewed, Steps: []*runnerv1.StepState{{StartedAt: skewed}}}
|
|
before := timeutil.TimeStampNow()
|
|
_, err := UpdateTaskByState(t.Context(), task.RunnerID, finalState)
|
|
require.Error(t, err)
|
|
assert.Equal(t, StatusRunning, unittest.AssertExistsAndLoadBean(t, &ActionTask{ID: task.ID}).Status)
|
|
assert.Equal(t, StatusRunning, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID}).Status)
|
|
_, err = UpdateTaskByState(t.Context(), task.RunnerID, finalState)
|
|
require.NoError(t, err)
|
|
gotJob := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
|
assert.Equal(t, StatusSuccess, gotJob.Status)
|
|
assert.GreaterOrEqual(t, gotJob.Stopped, before)
|
|
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) {
|
|
t.Helper()
|
|
|
|
run := &ActionRun{
|
|
Title: "cancelling-test-run",
|
|
RepoID: 1,
|
|
OwnerID: 2,
|
|
WorkflowID: "test.yaml",
|
|
Index: 999,
|
|
TriggerUserID: 2,
|
|
Ref: "refs/heads/master",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
Event: "push",
|
|
TriggerEvent: "push",
|
|
Status: StatusRunning,
|
|
Started: timeutil.TimeStampNow(),
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), run))
|
|
|
|
job := &ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: name,
|
|
Attempt: 1,
|
|
JobID: name,
|
|
Status: StatusRunning,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), job))
|
|
|
|
runner := &ActionRunner{
|
|
UUID: name,
|
|
Name: name,
|
|
HasCancellingSupport: hasCancellingSupport,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), runner))
|
|
|
|
task := &ActionTask{
|
|
JobID: job.ID,
|
|
Attempt: 1,
|
|
RunnerID: runner.ID,
|
|
Status: StatusRunning,
|
|
Started: timeutil.TimeStampNow(),
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), task))
|
|
|
|
job.TaskID = task.ID
|
|
_, err := UpdateRunJob(t.Context(), job, nil, "task_id")
|
|
require.NoError(t, err)
|
|
|
|
return task, job
|
|
}
|