mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
Allow to cancel on failure (#6158)
This commit is contained in:
@@ -4681,6 +4681,9 @@ const docTemplate = `{
|
||||
"CancelInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canceled_by_step": {
|
||||
"type": "string"
|
||||
},
|
||||
"canceled_by_user": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,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",
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PipelineError<D = unknown> {
|
||||
|
||||
export interface CancelInfo {
|
||||
canceled_by_user: string;
|
||||
canceled_by_step: string;
|
||||
superseded_by: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@
|
||||
<template v-else-if="pipeline.cancel_info.canceled_by_user">
|
||||
{{ $t('repo.pipeline.cancel_info.canceled_by_user', { user: pipeline.cancel_info.canceled_by_user }) }}
|
||||
</template>
|
||||
<template v-else-if="pipeline.cancel_info.canceled_by_step">
|
||||
{{ $t('repo.pipeline.cancel_info.canceled_by_step', { user: pipeline.cancel_info.canceled_by_step }) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user