diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index 68ce140b637..f3884edbb76 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -4681,6 +4681,9 @@ const docTemplate = `{ "CancelInfo": { "type": "object", "properties": { + "canceled_by_step": { + "type": "string" + }, "canceled_by_user": { "type": "string" }, diff --git a/docs/docs/20-usage/20-workflow-syntax.md b/docs/docs/20-usage/20-workflow-syntax.md index 12a02200a0a..6b1cc504991 100644 --- a/docs/docs/20-usage/20-workflow-syntax.md +++ b/docs/docs/20-usage/20-workflow-syntax.md @@ -196,6 +196,8 @@ Some of the steps may be allowed to fail without causing the whole workflow and + failure: ignore ``` +If you would like to cancel the full pipeline once the step fails, you can set `failure: cancel`. + ### `when` - Conditional Execution Woodpecker supports defining a list of conditions for a step by using a `when` block. If at least one of the conditions in the `when` block evaluate to true the step is executed, otherwise it is skipped. A condition is evaluated to true if _all_ sub-conditions are true. diff --git a/pipeline/backend/types/step.go b/pipeline/backend/types/step.go index 57adde71553..2b83dede979 100644 --- a/pipeline/backend/types/step.go +++ b/pipeline/backend/types/step.go @@ -40,7 +40,7 @@ type Step struct { OnFailure bool `json:"on_failure,omitempty"` OnSuccess bool `json:"on_success,omitempty"` Failure string `json:"failure,omitempty"` - AuthConfig Auth `json:"auth_config,omitempty"` + AuthConfig Auth `json:"auth_config"` NetworkMode string `json:"network_mode,omitempty"` Ports []Port `json:"ports,omitempty"` BackendOptions map[string]any `json:"backend_options,omitempty"` diff --git a/pipeline/frontend/yaml/linter/schema/schema.json b/pipeline/frontend/yaml/linter/schema/schema.json index c4903c84694..53bb63eecbc 100644 --- a/pipeline/frontend/yaml/linter/schema/schema.json +++ b/pipeline/frontend/yaml/linter/schema/schema.json @@ -335,7 +335,7 @@ "failure": { "description": "How to handle the failure of this step. Read more: https://woodpecker-ci.org/docs/usage/workflow-syntax#failure", "type": "string", - "enum": ["fail", "ignore"], + "enum": ["fail", "ignore", "cancel"], "default": "fail" }, "backend_options": { diff --git a/server/model/pipeline.go b/server/model/pipeline.go index 4de1c69bd36..1f67a1b0fa2 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -90,4 +90,5 @@ type PipelineOptions struct { type CancelInfo struct { CanceledByUser string `json:"canceled_by_user,omitempty"` SupersededBy int64 `json:"superseded_by,omitempty"` + CanceledByStep string `json:"canceled_by_step,omitempty"` } // @name CancelInfo diff --git a/server/model/step.go b/server/model/step.go index e6189f095b3..5091882fb1a 100644 --- a/server/model/step.go +++ b/server/model/step.go @@ -19,9 +19,7 @@ package model const ( FailureIgnore = "ignore" FailureFail = "fail" - //nolint:godot - // TODO: Not implemented yet. - // FailureCancel = "cancel" + FailureCancel = "cancel" ) // Step represents a process in the pipeline. diff --git a/server/pipeline/step_status.go b/server/pipeline/step_status.go index 5d2b5ddf944..6b3ee0c96a7 100644 --- a/server/pipeline/step_status.go +++ b/server/pipeline/step_status.go @@ -16,18 +16,20 @@ package pipeline import ( + "context" "fmt" "time" "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v3/rpc" + "go.woodpecker-ci.org/woodpecker/v3/server" "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/store" ) // UpdateStepStatus updates step status based on agent reports via RPC. -func UpdateStepStatus(store store.Store, step *model.Step, state rpc.StepState) error { +func UpdateStepStatus(ctx context.Context, store store.Store, step *model.Step, state rpc.StepState) error { log.Debug().Str("StepUUID", step.UUID).Msgf("Update step %#v state %#v", *step, state) switch step.State { @@ -54,6 +56,14 @@ func UpdateStepStatus(store store.Store, step *model.Step, state rpc.StepState) step.State = model.StatusSuccess } else { step.State = model.StatusFailure + + if step.Failure == model.FailureCancel { + // cancel the pipeline + err := cancelPipelineFromStep(ctx, store, step) + if err != nil { + return err + } + } } } @@ -71,6 +81,14 @@ func UpdateStepStatus(store store.Store, step *model.Step, state rpc.StepState) step.State = model.StatusSuccess } else { step.State = model.StatusFailure + + if step.Failure == model.FailureCancel { + // cancel the pipeline + err := cancelPipelineFromStep(ctx, store, step) + if err != nil { + return err + } + } } } @@ -89,6 +107,31 @@ func UpdateStepStatus(store store.Store, step *model.Step, state rpc.StepState) return store.StepUpdate(step) } +func cancelPipelineFromStep(ctx context.Context, store store.Store, step *model.Step) error { + pipeline, err := store.GetPipeline(step.PipelineID) + if err != nil { + return err + } + + repo, err := store.GetRepo(pipeline.RepoID) + if err != nil { + return err + } + + repoUser, err := store.GetUser(repo.UserID) + if err != nil { + return err + } + + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + return err + } + return Cancel(ctx, _forge, store, repo, repoUser, pipeline, &model.CancelInfo{ + CanceledByStep: step.Name, + }) +} + func UpdateStepToStatusSkipped(store store.Store, step model.Step, finished int64, status model.StatusValue) (*model.Step, error) { step.State = status if step.Started != 0 { diff --git a/server/pipeline/step_status_test.go b/server/pipeline/step_status_test.go index 52db2eb8f56..72304b9ae62 100644 --- a/server/pipeline/step_status_test.go +++ b/server/pipeline/step_status_test.go @@ -47,7 +47,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusPending} state := rpc.StepState{Started: 42, Finished: 0} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusRunning, step.State) @@ -60,7 +60,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusPending} state := rpc.StepState{Started: 0, Finished: 0} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusRunning, step.State) @@ -76,7 +76,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusPending} state := rpc.StepState{Started: 42, Exited: true, Finished: 100, ExitCode: 0, Error: ""} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusSuccess, step.State) @@ -89,7 +89,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusPending} state := rpc.StepState{Started: 42, Exited: true, Finished: 0, ExitCode: 0, Error: ""} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusSuccess, step.State) @@ -105,7 +105,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusPending} state := rpc.StepState{Started: 42, Exited: true, Finished: 34, ExitCode: 1, Error: "an error"} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusFailure, step.State) @@ -126,7 +126,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Exited: true, Finished: 100, ExitCode: 0, Error: ""} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusSuccess, step.State) @@ -138,7 +138,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Exited: true, Finished: 0, ExitCode: 0, Error: ""} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusSuccess, step.State) @@ -154,7 +154,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Exited: true, Finished: 34, ExitCode: pipeline.ExitCodeKilled, Error: "an error"} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusFailure, step.State) @@ -167,7 +167,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Exited: true, Finished: 34, ExitCode: 0, Error: "an error"} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusFailure, step.State) @@ -180,7 +180,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Exited: false, Finished: 0} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusRunning, step.State) @@ -196,7 +196,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Canceled: true} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusKilled, step.State) @@ -208,7 +208,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusRunning, Started: 42} state := rpc.StepState{Canceled: true, Exited: true, Finished: 100, ExitCode: 1, Error: "canceled"} - err := UpdateStepStatus(mockStoreStep(t), step, state) + err := UpdateStepStatus(t.Context(), mockStoreStep(t), step, state) assert.NoError(t, err) assert.Equal(t, model.StatusKilled, step.State) @@ -223,7 +223,7 @@ func TestUpdateStepStatus(t *testing.T) { step := &model.Step{State: model.StatusKilled, Started: 42, Finished: 64} state := rpc.StepState{Exited: false} - err := UpdateStepStatus(mocks.NewMockStore(t), step, state) + err := UpdateStepStatus(t.Context(), mocks.NewMockStore(t), step, state) assert.Error(t, err) assert.Contains(t, err.Error(), "does not expect rpc state updates") diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go index 6680404f2c1..d02b72e9493 100644 --- a/server/rpc/rpc.go +++ b/server/rpc/rpc.go @@ -195,7 +195,7 @@ func (s *RPC) Update(c context.Context, strWorkflowID string, state rpc.StepStat return err } - if err := pipeline.UpdateStepStatus(s.store, step, state); err != nil { + if err := pipeline.UpdateStepStatus(c, s.store, step, state); err != nil { log.Error().Err(err).Msg("rpc.update: cannot update step") } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 76d31055a64..47ded2e1de9 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -271,7 +271,8 @@ "created": "Created: {created}", "cancel_info": { "superseded_by": "Superseded by #{pipelineId}", - "canceled_by_user": "Canceled by {user}" + "canceled_by_user": "Canceled by {user}", + "canceled_by_step": "Canceled due to {step}" }, "debug": { "title": "Debug", diff --git a/web/src/lib/api/types/pipeline.ts b/web/src/lib/api/types/pipeline.ts index 1a3af7817db..2eaf8e1f967 100644 --- a/web/src/lib/api/types/pipeline.ts +++ b/web/src/lib/api/types/pipeline.ts @@ -9,6 +9,7 @@ export interface PipelineError { export interface CancelInfo { canceled_by_user: string; + canceled_by_step: string; superseded_by: number; } diff --git a/web/src/views/repo/pipeline/PipelineWrapper.vue b/web/src/views/repo/pipeline/PipelineWrapper.vue index ca98a7243ad..5007b5e82d1 100644 --- a/web/src/views/repo/pipeline/PipelineWrapper.vue +++ b/web/src/views/repo/pipeline/PipelineWrapper.vue @@ -91,6 +91,9 @@ +