Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/server/openapi/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4681,6 +4681,9 @@ const docTemplate = `{
"CancelInfo": {
"type": "object",
"properties": {
"canceled_by_step": {
"type": "string"
},
"canceled_by_user": {
"type": "string"
},
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/20-usage/20-workflow-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pipeline/backend/types/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion pipeline/frontend/yaml/linter/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions server/model/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 1 addition & 3 deletions server/model/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 44 additions & 1 deletion server/pipeline/step_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}
}
}

Expand All @@ -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
}
}
}
}

Expand All @@ -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 {
Expand Down
26 changes: 13 additions & 13 deletions server/pipeline/step_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion server/rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
3 changes: 2 additions & 1 deletion web/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/api/types/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface PipelineError<D = unknown> {

export interface CancelInfo {
canceled_by_user: string;
canceled_by_step: string;
superseded_by: number;
}

Expand Down
3 changes: 3 additions & 0 deletions web/src/views/repo/pipeline/PipelineWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down