From 083c0a1b4ae8db55cb22180aea4750ca89413e05 Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Wed, 28 Jan 2026 23:46:18 +0100 Subject: [PATCH 01/20] first itteration: detached steps / services report back --- pipeline/pipeline.go | 60 ++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go index 93e8f9467a4..60a733964ff 100644 --- a/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -233,23 +233,43 @@ func (r *Runtime) execAll(runnerCtx context.Context, steps []*backend.Step) <-ch Str("step", step.Name). Msg("executing") - processState, err := r.exec(runnerCtx, step) + // setup exec func in a way it can be detached if needed + // wg will signal once + execAndTrace := func(wg *sync.WaitGroup) error { + processState, err := r.exec(runnerCtx, step, wg) - logger.Debug(). - Str("step", step.Name). - Msg("complete") + logger.Debug(). + Str("step", step.Name). + Msg("complete") - // normalize context cancel error - if errors.Is(err, context.Canceled) { - err = ErrCancel + // normalize context cancel error + if errors.Is(err, context.Canceled) { + err = ErrCancel + } + + // Return the error after tracing it. + err = r.traceStep(processState, err, step) + if err != nil && step.Failure == metadata.FailureIgnore { + return nil + } + return err } - // Return the error after tracing it. - err = r.traceStep(processState, err, step) - if err != nil && step.Failure == metadata.FailureIgnore { - return nil + // we report all errors till setup happened + // afterwards they just ged dropped + if step.Detached { + var wg sync.WaitGroup + wg.Add(1) + var setupErr error + go func() { + setupErr = execAndTrace(&wg) + }() + wg.Wait() + return setupErr } - return err + + // run blocking + return execAndTrace(nil) }) } @@ -262,7 +282,13 @@ func (r *Runtime) execAll(runnerCtx context.Context, steps []*backend.Step) <-ch } // Executes the step and returns the state and error. -func (r *Runtime) exec(runnerCtx context.Context, step *backend.Step) (*backend.State, error) { +func (r *Runtime) exec(runnerCtx context.Context, step *backend.Step, setupWg *sync.WaitGroup) (*backend.State, error) { + defer func() { + if setupWg != nil { + setupWg.Done() + } + }() + if err := r.engine.StartStep(r.ctx, step, r.taskUUID); err != nil { //nolint:contextcheck return nil, err } @@ -287,9 +313,11 @@ func (r *Runtime) exec(runnerCtx context.Context, step *backend.Step) (*backend. }() } - // nothing else to do, this is a detached process. - if step.Detached { - return nil, nil + // nothing else to block for detached process. + if setupWg != nil { + setupWg.Done() + // set to nil so the setupWg.Done in defer does not call it a second time + setupWg = nil } // We wait until all data was logged. (Needed for some backends like local as WaitStep kills the log stream) From 3df799a514f60f42237d7c1c886df5f8fe4ae13f Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 30 Jan 2026 16:28:13 +0100 Subject: [PATCH 02/20] backend docker: first softKill containers still running --- pipeline/backend/docker/docker.go | 16 +++++++++++++++- pipeline/backend/dummy/dummy.go | 2 +- pipeline/backend/local/local.go | 3 +++ pipeline/backend/types/backend.go | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pipeline/backend/docker/docker.go b/pipeline/backend/docker/docker.go index 8a48fdd72e6..2704e863632 100644 --- a/pipeline/backend/docker/docker.go +++ b/pipeline/backend/docker/docker.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/containerd/errdefs" "github.com/docker/docker/api/types/container" @@ -41,6 +42,8 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/shared/utils" ) +var containerKillTimeout = time.Second + type docker struct { client client.APIClient info system.Info @@ -305,6 +308,12 @@ func (e *docker) DestroyStep(ctx context.Context, step *backend.Step, taskUUID s containerName := toContainerName(step) + softKillCtx, _ := context.WithTimeout(ctx, containerKillTimeout) //nolint:govet + if err := e.client.ContainerKill(softKillCtx, containerName, ""); err != nil && !isErrContainerNotFoundOrNotRunning(err) { + return err + } + + // if soft kill did now work just force kill it if err := e.client.ContainerKill(ctx, containerName, "9"); err != nil && !isErrContainerNotFoundOrNotRunning(err) { return err } @@ -349,8 +358,13 @@ func isErrContainerNotFoundOrNotRunning(err error) bool { // Error response from daemon: Cannot kill container: ...: No such container: ... // Error response from daemon: Cannot kill container: ...: Container ... is not running" // Error response from podman daemon: can only kill running containers. ... is in state exited + // Error response from daemon: removal of container ... is already in progress // Error: No such container: ... - return err != nil && (strings.Contains(err.Error(), "No such container") || strings.Contains(err.Error(), "is not running") || strings.Contains(err.Error(), "can only kill running containers")) + return err != nil && + (strings.Contains(err.Error(), "No such container") || + strings.Contains(err.Error(), "is not running") || + strings.Contains(err.Error(), "can only kill running containers") || + (strings.Contains(err.Error(), "removal of container") && strings.Contains(err.Error(), "is already in progress"))) } // normalizeArchType converts the arch type reported by docker info into diff --git a/pipeline/backend/dummy/dummy.go b/pipeline/backend/dummy/dummy.go index 650b63dcb18..69eb8d5aa59 100644 --- a/pipeline/backend/dummy/dummy.go +++ b/pipeline/backend/dummy/dummy.go @@ -200,7 +200,7 @@ func (e *dummy) DestroyStep(_ context.Context, step *backend.Step, taskUUID stri _, exist := e.kv.Load("task_" + taskUUID) if !exist { - return fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID) + return nil } // check state diff --git a/pipeline/backend/local/local.go b/pipeline/backend/local/local.go index 2529698199f..f6c13a2ef11 100644 --- a/pipeline/backend/local/local.go +++ b/pipeline/backend/local/local.go @@ -231,6 +231,9 @@ func (e *local) TailStep(_ context.Context, step *types.Step, taskUUID string) ( func (e *local) DestroyStep(_ context.Context, step *types.Step, taskUUID string) error { state, err := e.getStepState(taskUUID, step.UUID) if err != nil { + if errors.Is(err, ErrStepStateNotFound) { + return nil + } return err } diff --git a/pipeline/backend/types/backend.go b/pipeline/backend/types/backend.go index 229174c1dc9..0877c422a12 100644 --- a/pipeline/backend/types/backend.go +++ b/pipeline/backend/types/backend.go @@ -142,6 +142,7 @@ type Backend interface { // - Clean up step-specific resources (containers, processes) // - Close any open log streams // - Not affect other steps in the same or other workflows + // - Must not fail if already invoked once // // Must be safe to call even if StartStep failed or the step was never started. // This function must be thread-safe for concurrent calls. From bd851e2c1b289231649a044c442c7f83ceb8761f Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 30 Jan 2026 16:32:23 +0100 Subject: [PATCH 03/20] use stop --- pipeline/backend/docker/docker.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pipeline/backend/docker/docker.go b/pipeline/backend/docker/docker.go index 2704e863632..1b671f57c6b 100644 --- a/pipeline/backend/docker/docker.go +++ b/pipeline/backend/docker/docker.go @@ -21,7 +21,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/containerd/errdefs" "github.com/docker/docker/api/types/container" @@ -42,7 +41,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/shared/utils" ) -var containerKillTimeout = time.Second +var containerKillTimeout = 1 // seconds type docker struct { client client.APIClient @@ -308,8 +307,9 @@ func (e *docker) DestroyStep(ctx context.Context, step *backend.Step, taskUUID s containerName := toContainerName(step) - softKillCtx, _ := context.WithTimeout(ctx, containerKillTimeout) //nolint:govet - if err := e.client.ContainerKill(softKillCtx, containerName, ""); err != nil && !isErrContainerNotFoundOrNotRunning(err) { + if err := e.client.ContainerStop(ctx, containerName, container.StopOptions{ + Timeout: &containerKillTimeout, + }); err != nil && !isErrContainerNotFoundOrNotRunning(err) { return err } From 8e7ae4bff2425a7e1a308ca469180dc44719fe69 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 30 Jan 2026 17:41:10 +0100 Subject: [PATCH 04/20] docker backend: refactor DestroyStep --- pipeline/backend/docker/docker.go | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pipeline/backend/docker/docker.go b/pipeline/backend/docker/docker.go index 1b671f57c6b..422656d6e20 100644 --- a/pipeline/backend/docker/docker.go +++ b/pipeline/backend/docker/docker.go @@ -16,6 +16,8 @@ package docker import ( "context" + "errors" + "fmt" "io" "net/http" "os" @@ -35,13 +37,14 @@ import ( "github.com/moby/term" "github.com/rs/zerolog/log" "github.com/urfave/cli/v3" + "golang.org/x/sync/errgroup" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" "go.woodpecker-ci.org/woodpecker/v3/shared/httputil" "go.woodpecker-ci.org/woodpecker/v3/shared/utils" ) -var containerKillTimeout = 1 // seconds +var containerKillTimeout = 5 // seconds type docker struct { client client.APIClient @@ -306,20 +309,24 @@ func (e *docker) DestroyStep(ctx context.Context, step *backend.Step, taskUUID s log.Trace().Str("taskUUID", taskUUID).Msgf("stop step %s", step.Name) containerName := toContainerName(step) + var stopErr error + // we first signal to the container to stop ... if err := e.client.ContainerStop(ctx, containerName, container.StopOptions{ Timeout: &containerKillTimeout, }); err != nil && !isErrContainerNotFoundOrNotRunning(err) { - return err + // we do not return error jet as we try to kill it first + stopErr = fmt.Errorf("could not stop container '%s': %w", step.Name, err) } - // if soft kill did now work just force kill it + // ... and if stop does not work just force kill it if err := e.client.ContainerKill(ctx, containerName, "9"); err != nil && !isErrContainerNotFoundOrNotRunning(err) { - return err + return errors.Join(stopErr, fmt.Errorf("could not kill container '%s': %w", step.Name, err)) } + // now we clean up files left if err := e.client.ContainerRemove(ctx, containerName, removeOpts); err != nil && !isErrContainerNotFoundOrNotRunning(err) { - return err + return fmt.Errorf("could not remove container '%s': %w", step.Name, err) } return nil @@ -328,17 +335,20 @@ func (e *docker) DestroyStep(ctx context.Context, step *backend.Step, taskUUID s func (e *docker) DestroyWorkflow(ctx context.Context, conf *backend.Config, taskUUID string) error { log.Trace().Str("taskUUID", taskUUID).Msgf("delete workflow environment") + errWG := errgroup.Group{} + for _, stage := range conf.Stages { for _, step := range stage.Steps { - containerName := toContainerName(step) - if err := e.client.ContainerKill(ctx, containerName, "9"); err != nil && !isErrContainerNotFoundOrNotRunning(err) { - log.Error().Err(err).Msgf("could not kill container '%s'", step.Name) - } - if err := e.client.ContainerRemove(ctx, containerName, removeOpts); err != nil && !isErrContainerNotFoundOrNotRunning(err) { - log.Error().Err(err).Msgf("could not remove container '%s'", step.Name) - } + errWG.Go(func() error { + return e.DestroyStep(ctx, step, taskUUID) + }) } } + + if err := errWG.Wait(); err != nil { + log.Error().Err(err).Msgf("could not destroy all containers") + } + if err := e.client.VolumeRemove(ctx, conf.Volume, true); err != nil { log.Error().Err(err).Msgf("could not remove volume '%s'", conf.Volume) } From ec28ee6c292f7291e73a9c5661a07b2a80a2de08 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 30 Jan 2026 18:23:35 +0100 Subject: [PATCH 05/20] queue: enhance error logs in agents --- server/queue/fifo.go | 3 +++ server/queue/queue.go | 30 ++++++++++++++++++++++++++++++ server/rpc/rpc.go | 5 ++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/queue/fifo.go b/server/queue/fifo.go index 3b370186ebe..bde6ae1cc51 100644 --- a/server/queue/fifo.go +++ b/server/queue/fifo.go @@ -134,6 +134,9 @@ func (q *fifo) finished(ids []string, exitStatus model.StatusValue, err error) e q.Lock() defer q.Unlock() + // it's an external error so we wrap it + err = NewErrExternal(err) + var errs []error // we first process the tasks itself for _, id := range ids { diff --git a/server/queue/queue.go b/server/queue/queue.go index 77fa3ed8ec8..2dcb7925946 100644 --- a/server/queue/queue.go +++ b/server/queue/queue.go @@ -41,6 +41,36 @@ var ( ErrWorkerKicked = errors.New("worker was kicked") ) +// ErrExternal wraps an external error +type ErrExternal struct { + err error +} + +func (e *ErrExternal) Error() string { + if e.err != nil { + return "external error: " + e.err.Error() + } + return "external error" +} + +// Unwrap allows errors.Is and errors.As to work with the wrapped error +func (e *ErrExternal) Unwrap() error { + return e.err +} + +// Is allows errors.Is to match against ErrExternal types +func (e *ErrExternal) Is(target error) bool { + _, ok := target.(*ErrExternal) + return ok +} + +func NewErrExternal(err error) error { + if err == nil { + return nil + } + return &ErrExternal{err: err} +} + // InfoT provides runtime information. type InfoT struct { Pending []*model.Task `json:"pending"` diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go index 5ff78e275be..99b04394bfd 100644 --- a/server/rpc/rpc.go +++ b/server/rpc/rpc.go @@ -118,7 +118,10 @@ func (s *RPC) Wait(c context.Context, workflowID string) (canceled bool, err err // we explicit send a cancel signal return true, nil } - // unknown error happened + if errors.Is(err, new(queue.ErrExternal)) { + // we do not have to give back the error an agent already told us + return false, nil + } return false, err } From 26c38edaa8d79a838916f99e6cd53fae128b3188 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 30 Jan 2026 18:28:47 +0100 Subject: [PATCH 06/20] fix code comment --- server/queue/fifo.go | 2 +- server/rpc/rpc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/queue/fifo.go b/server/queue/fifo.go index bde6ae1cc51..a25581d97e7 100644 --- a/server/queue/fifo.go +++ b/server/queue/fifo.go @@ -134,7 +134,7 @@ func (q *fifo) finished(ids []string, exitStatus model.StatusValue, err error) e q.Lock() defer q.Unlock() - // it's an external error so we wrap it + // it's an external error so we wrap it err = NewErrExternal(err) var errs []error diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go index 99b04394bfd..e1dbebf80ce 100644 --- a/server/rpc/rpc.go +++ b/server/rpc/rpc.go @@ -119,7 +119,7 @@ func (s *RPC) Wait(c context.Context, workflowID string) (canceled bool, err err return true, nil } if errors.Is(err, new(queue.ErrExternal)) { - // we do not have to give back the error an agent already told us + // we do not have to give back the same error the agent already told us return false, nil } return false, err From f50de70db336fa220c4e1bd1346cf8f702845902 Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Thu, 5 Feb 2026 23:23:12 +0100 Subject: [PATCH 07/20] move into its own pull: https://github.com/woodpecker-ci/woodpecker/pull/6056 --- server/queue/fifo.go | 3 --- server/queue/queue.go | 30 ------------------------------ server/rpc/rpc.go | 5 +---- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/server/queue/fifo.go b/server/queue/fifo.go index a25581d97e7..3b370186ebe 100644 --- a/server/queue/fifo.go +++ b/server/queue/fifo.go @@ -134,9 +134,6 @@ func (q *fifo) finished(ids []string, exitStatus model.StatusValue, err error) e q.Lock() defer q.Unlock() - // it's an external error so we wrap it - err = NewErrExternal(err) - var errs []error // we first process the tasks itself for _, id := range ids { diff --git a/server/queue/queue.go b/server/queue/queue.go index 2dcb7925946..77fa3ed8ec8 100644 --- a/server/queue/queue.go +++ b/server/queue/queue.go @@ -41,36 +41,6 @@ var ( ErrWorkerKicked = errors.New("worker was kicked") ) -// ErrExternal wraps an external error -type ErrExternal struct { - err error -} - -func (e *ErrExternal) Error() string { - if e.err != nil { - return "external error: " + e.err.Error() - } - return "external error" -} - -// Unwrap allows errors.Is and errors.As to work with the wrapped error -func (e *ErrExternal) Unwrap() error { - return e.err -} - -// Is allows errors.Is to match against ErrExternal types -func (e *ErrExternal) Is(target error) bool { - _, ok := target.(*ErrExternal) - return ok -} - -func NewErrExternal(err error) error { - if err == nil { - return nil - } - return &ErrExternal{err: err} -} - // InfoT provides runtime information. type InfoT struct { Pending []*model.Task `json:"pending"` diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go index e1dbebf80ce..5ff78e275be 100644 --- a/server/rpc/rpc.go +++ b/server/rpc/rpc.go @@ -118,10 +118,7 @@ func (s *RPC) Wait(c context.Context, workflowID string) (canceled bool, err err // we explicit send a cancel signal return true, nil } - if errors.Is(err, new(queue.ErrExternal)) { - // we do not have to give back the same error the agent already told us - return false, nil - } + // unknown error happened return false, err } From 8ece665b7cbcc783714bc88ea7a96b765db819cf Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 00:37:54 +0100 Subject: [PATCH 08/20] update test acordingly --- pipeline/backend/dummy/dummy_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pipeline/backend/dummy/dummy_test.go b/pipeline/backend/dummy/dummy_test.go index c0d78d656a9..90a3555b3c0 100644 --- a/pipeline/backend/dummy/dummy_test.go +++ b/pipeline/backend/dummy/dummy_test.go @@ -47,9 +47,6 @@ func TestSmalPipelineDummyRun(t *testing.T) { _, err = dummyEngine.WaitStep(ctx, step, nonExistWorkflowID) assert.Error(t, err) - - err = dummyEngine.DestroyStep(ctx, step, nonExistWorkflowID) - assert.Error(t, err) }) t.Run("step exec successfully", func(t *testing.T) { From 5e657c947bd232698481e4b5aedc7ee8963ad181 Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 05:33:32 +0100 Subject: [PATCH 09/20] pipeline.errors ... prepare --- pipeline/errors/{error.go => linter.go} | 14 ++++++++++++++ pipeline/errors/{error_test.go => linter_test.go} | 14 ++++++++++++++ pipeline/errors/runtime.go | 15 +++++++++++++++ 3 files changed, 43 insertions(+) rename pipeline/errors/{error.go => linter.go} (68%) rename pipeline/errors/{error_test.go => linter_test.go} (81%) create mode 100644 pipeline/errors/runtime.go diff --git a/pipeline/errors/error.go b/pipeline/errors/linter.go similarity index 68% rename from pipeline/errors/error.go rename to pipeline/errors/linter.go index 4adf7fc623c..7b2462b0970 100644 --- a/pipeline/errors/error.go +++ b/pipeline/errors/linter.go @@ -1,3 +1,17 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package errors import ( diff --git a/pipeline/errors/error_test.go b/pipeline/errors/linter_test.go similarity index 81% rename from pipeline/errors/error_test.go rename to pipeline/errors/linter_test.go index a1658a79706..2f7db556b64 100644 --- a/pipeline/errors/error_test.go +++ b/pipeline/errors/linter_test.go @@ -1,3 +1,17 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package errors_test import ( diff --git a/pipeline/errors/runtime.go b/pipeline/errors/runtime.go new file mode 100644 index 00000000000..d4c579d8d53 --- /dev/null +++ b/pipeline/errors/runtime.go @@ -0,0 +1,15 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors From 5b1a3d736470b7f3daacbdc1f72d3b89ca520e0c Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 06:02:05 +0100 Subject: [PATCH 10/20] move errors --- agent/runner.go | 9 +-- agent/tracer.go | 3 +- pipeline/error.go | 52 --------------- pipeline/error_test.go | 36 ----------- pipeline/errors/linter.go | 16 ++--- pipeline/errors/linter_test.go | 37 ++++++----- pipeline/errors/runtime.go | 37 +++++++++++ pipeline/errors/types/errors.go | 40 ------------ pipeline/frontend/yaml/linter/error.go | 11 ++-- pipeline/frontend/yaml/linter/linter.go | 6 +- pipeline/frontend/yaml/matrix/matrix.go | 6 +- pipeline/pipeline.go | 16 ++--- server/model/pipeline.go | 72 ++++++++++----------- server/pipeline/stepbuilder/step_builder.go | 3 +- 14 files changed, 125 insertions(+), 219 deletions(-) delete mode 100644 pipeline/error.go delete mode 100644 pipeline/error_test.go delete mode 100644 pipeline/errors/types/errors.go diff --git a/agent/runner.go b/agent/runner.go index 9d3e205d57a..bf914595a93 100644 --- a/agent/runner.go +++ b/agent/runner.go @@ -27,6 +27,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/rpc" "go.woodpecker-ci.org/woodpecker/v3/shared/constant" "go.woodpecker-ci.org/woodpecker/v3/shared/utils" @@ -99,7 +100,7 @@ func (r *Runner) Run(runnerCtx, shutdownCtx context.Context) error { workflowCtx = utils.WithContextSigtermCallback(workflowCtx, func() { logger.Error().Msg("received sigterm termination signal") // WithContextSigtermCallback would cancel the context too, but we want our own custom error - cancelWorkflowCtx(pipeline.ErrCancel) + cancelWorkflowCtx(pipeline_errors.ErrCancel) }) // Listen for remote cancel events (UI / API). @@ -114,7 +115,7 @@ func (r *Runner) Run(runnerCtx, shutdownCtx context.Context) error { } else { if canceled { logger.Debug().Err(err).Msg("server side cancel signal received") - cancelWorkflowCtx(pipeline.ErrCancel) + cancelWorkflowCtx(pipeline_errors.ErrCancel) } // Wait returned without error, meaning the workflow finished normally logger.Debug().Msg("cancel listener exited normally") @@ -171,10 +172,10 @@ func (r *Runner) Run(runnerCtx, shutdownCtx context.Context) error { if err != nil { state.Error = err.Error() - if errors.Is(err, pipeline.ErrCancel) { + if errors.Is(err, pipeline_errors.ErrCancel) { state.Canceled = true // cleanup joined error messages - state.Error = pipeline.ErrCancel.Error() + state.Error = pipeline_errors.ErrCancel.Error() } } diff --git a/agent/tracer.go b/agent/tracer.go index 69a9f653963..b5f7907b724 100644 --- a/agent/tracer.go +++ b/agent/tracer.go @@ -25,6 +25,7 @@ import ( "github.com/rs/zerolog" "go.woodpecker-ci.org/woodpecker/v3/pipeline" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/rpc" ) @@ -46,7 +47,7 @@ func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, Exited: state.Process.Exited, ExitCode: state.Process.ExitCode, Started: state.Process.Started, - Canceled: errors.Is(state.Process.Error, pipeline.ErrCancel), + Canceled: errors.Is(state.Process.Error, pipeline_errors.ErrCancel), } if state.Process.Error != nil { stepState.Error = state.Process.Error.Error() diff --git a/pipeline/error.go b/pipeline/error.go deleted file mode 100644 index d2b39c33585..00000000000 --- a/pipeline/error.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2023 Woodpecker Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pipeline - -import ( - "errors" - "fmt" -) - -var ( - // ErrSkip is used as a return value when container execution should be - // skipped at runtime. It is not returned as an error by any function. - ErrSkip = errors.New("Skipped") - - // ErrCancel is used as a return value when the container execution receives - // a cancellation signal from the context. - ErrCancel = errors.New("Canceled") -) - -// An ExitError reports an unsuccessful exit. -type ExitError struct { - UUID string - Code int -} - -// Error returns the error message in string format. -func (e *ExitError) Error() string { - return fmt.Sprintf("uuid=%s: exit code %d", e.UUID, e.Code) -} - -// An OomError reports the process received an OOMKill from the kernel. -type OomError struct { - UUID string - Code int -} - -// Error returns the error message in string format. -func (e *OomError) Error() string { - return fmt.Sprintf("uuid=%s: received oom kill", e.UUID) -} diff --git a/pipeline/error_test.go b/pipeline/error_test.go deleted file mode 100644 index 95a264f2e6c..00000000000 --- a/pipeline/error_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2023 Woodpecker Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pipeline - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExitError(t *testing.T) { - err := ExitError{ - UUID: "14534321", - Code: 255, - } - assert.Equal(t, "uuid=14534321: exit code 255", err.Error()) -} - -func TestOomError(t *testing.T) { - err := OomError{ - UUID: "14534321", - } - assert.Equal(t, "uuid=14534321: received oom kill", err.Error()) -} diff --git a/pipeline/errors/linter.go b/pipeline/errors/linter.go index 7b2462b0970..76293e99765 100644 --- a/pipeline/errors/linter.go +++ b/pipeline/errors/linter.go @@ -18,8 +18,6 @@ import ( "errors" "go.uber.org/multierr" - - "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" ) type LinterErrorData struct { @@ -39,8 +37,8 @@ type BadHabitErrorData struct { Docs string `json:"docs"` } -func GetLinterData(e *types.PipelineError) *LinterErrorData { - if e.Type != types.PipelineErrorTypeLinter { +func GetLinterData(e *PipelineError) *LinterErrorData { + if e.Type != PipelineErrorTypeLinter { return nil } @@ -51,16 +49,16 @@ func GetLinterData(e *types.PipelineError) *LinterErrorData { return nil } -func GetPipelineErrors(err error) []*types.PipelineError { - var pipelineErrors []*types.PipelineError +func GetPipelineErrors(err error) []*PipelineError { + var pipelineErrors []*PipelineError for _, _err := range multierr.Errors(err) { - var err *types.PipelineError + var err *PipelineError if errors.As(_err, &err) { pipelineErrors = append(pipelineErrors, err) } else { - pipelineErrors = append(pipelineErrors, &types.PipelineError{ + pipelineErrors = append(pipelineErrors, &PipelineError{ Message: _err.Error(), - Type: types.PipelineErrorTypeGeneric, + Type: PipelineErrorTypeGeneric, }) } } diff --git a/pipeline/errors/linter_test.go b/pipeline/errors/linter_test.go index 2f7db556b64..4eefe0371f1 100644 --- a/pipeline/errors/linter_test.go +++ b/pipeline/errors/linter_test.go @@ -22,7 +22,6 @@ import ( "go.uber.org/multierr" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" - "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" ) func TestGetPipelineErrors(t *testing.T) { @@ -31,7 +30,7 @@ func TestGetPipelineErrors(t *testing.T) { tests := []struct { title string err error - expected []*types.PipelineError + expected []*pipeline_errors.PipelineError }{ { title: "nil error", @@ -40,10 +39,10 @@ func TestGetPipelineErrors(t *testing.T) { }, { title: "warning", - err: &types.PipelineError{ + err: &pipeline_errors.PipelineError{ IsWarning: true, }, - expected: []*types.PipelineError{ + expected: []*pipeline_errors.PipelineError{ { IsWarning: true, }, @@ -51,10 +50,10 @@ func TestGetPipelineErrors(t *testing.T) { }, { title: "pipeline error", - err: &types.PipelineError{ + err: &pipeline_errors.PipelineError{ IsWarning: false, }, - expected: []*types.PipelineError{ + expected: []*pipeline_errors.PipelineError{ { IsWarning: false, }, @@ -63,14 +62,14 @@ func TestGetPipelineErrors(t *testing.T) { { title: "multiple warnings", err: multierr.Combine( - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, ), - expected: []*types.PipelineError{ + expected: []*pipeline_errors.PipelineError{ { IsWarning: true, }, @@ -82,15 +81,15 @@ func TestGetPipelineErrors(t *testing.T) { { title: "multiple errors and warnings", err: multierr.Combine( - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: false, }, errors.New("some error"), ), - expected: []*types.PipelineError{ + expected: []*pipeline_errors.PipelineError{ { IsWarning: true, }, @@ -98,7 +97,7 @@ func TestGetPipelineErrors(t *testing.T) { IsWarning: false, }, { - Type: types.PipelineErrorTypeGeneric, + Type: pipeline_errors.PipelineErrorTypeGeneric, IsWarning: false, Message: "some error", }, @@ -126,14 +125,14 @@ func TestHasBlockingErrors(t *testing.T) { }, { title: "warning", - err: &types.PipelineError{ + err: &pipeline_errors.PipelineError{ IsWarning: true, }, expected: false, }, { title: "pipeline error", - err: &types.PipelineError{ + err: &pipeline_errors.PipelineError{ IsWarning: false, }, expected: true, @@ -141,10 +140,10 @@ func TestHasBlockingErrors(t *testing.T) { { title: "multiple warnings", err: multierr.Combine( - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, ), @@ -153,10 +152,10 @@ func TestHasBlockingErrors(t *testing.T) { { title: "multiple errors and warnings", err: multierr.Combine( - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: true, }, - &types.PipelineError{ + &pipeline_errors.PipelineError{ IsWarning: false, }, errors.New("some error"), diff --git a/pipeline/errors/runtime.go b/pipeline/errors/runtime.go index d4c579d8d53..3b7a29a46c0 100644 --- a/pipeline/errors/runtime.go +++ b/pipeline/errors/runtime.go @@ -13,3 +13,40 @@ // limitations under the License. package errors + +import ( + "errors" + "fmt" +) + +var ( + // ErrSkip is used as a return value when container execution should be + // skipped at runtime. It is not returned as an error by any function. + ErrSkip = errors.New("Skipped") + + // ErrCancel is used as a return value when the container execution receives + // a cancellation signal from the context. + ErrCancel = errors.New("Canceled") +) + +// An ExitError reports an unsuccessful exit. +type ExitError struct { + UUID string + Code int +} + +// Error returns the error message in string format. +func (e *ExitError) Error() string { + return fmt.Sprintf("uuid=%s: exit code %d", e.UUID, e.Code) +} + +// An OomError reports the process received an OOMKill from the kernel. +type OomError struct { + UUID string + Code int +} + +// Error returns the error message in string format. +func (e *OomError) Error() string { + return fmt.Sprintf("uuid=%s: received oom kill", e.UUID) +} diff --git a/pipeline/errors/types/errors.go b/pipeline/errors/types/errors.go deleted file mode 100644 index caa85c08ef6..00000000000 --- a/pipeline/errors/types/errors.go +++ /dev/null @@ -1,40 +0,0 @@ -package types - -import ( - "fmt" - - backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" -) - -type PipelineErrorType string - -const ( - PipelineErrorTypeLinter PipelineErrorType = "linter" // some error with the config syntax - PipelineErrorTypeDeprecation PipelineErrorType = "deprecation" // using some deprecated feature - PipelineErrorTypeCompiler PipelineErrorType = "compiler" // some error with the config semantics - PipelineErrorTypeGeneric PipelineErrorType = "generic" // some generic error - PipelineErrorTypeBadHabit PipelineErrorType = "bad_habit" // some bad-habit error -) - -type PipelineError struct { - Type PipelineErrorType `json:"type"` - Message string `json:"message"` - IsWarning bool `json:"is_warning"` - Data any `json:"data"` -} - -func (e *PipelineError) Error() string { - return fmt.Sprintf("[%s] %s", e.Type, e.Message) -} - -type ErrInvalidWorkflowSetup struct { - Err error - Step *backend.Step -} - -func (e *ErrInvalidWorkflowSetup) Error() string { - if e.Step != nil { - return fmt.Sprintf("error in workflow setup step '%s': %v", e.Step.Name, e.Err) - } - return fmt.Sprintf("error in workflow setup: %v", e.Err) -} diff --git a/pipeline/frontend/yaml/linter/error.go b/pipeline/frontend/yaml/linter/error.go index 96c96ddc0b2..b267773c91d 100644 --- a/pipeline/frontend/yaml/linter/error.go +++ b/pipeline/frontend/yaml/linter/error.go @@ -15,15 +15,14 @@ package linter import ( - "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" - errorTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" ) -func newLinterError(message, file, field string, isWarning bool) *errorTypes.PipelineError { - return &errorTypes.PipelineError{ - Type: errorTypes.PipelineErrorTypeLinter, +func newLinterError(message, file, field string, isWarning bool) *pipeline_errors.PipelineError { + return &pipeline_errors.PipelineError{ + Type: pipeline_errors.PipelineErrorTypeLinter, Message: message, - Data: &errors.LinterErrorData{File: file, Field: field}, + Data: &pipeline_errors.LinterErrorData{File: file, Field: field}, IsWarning: isWarning, } } diff --git a/pipeline/frontend/yaml/linter/linter.go b/pipeline/frontend/yaml/linter/linter.go index 4b991dc41b4..74da3661bab 100644 --- a/pipeline/frontend/yaml/linter/linter.go +++ b/pipeline/frontend/yaml/linter/linter.go @@ -21,7 +21,7 @@ import ( "go.uber.org/multierr" "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" - errorTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter/schema" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/utils" @@ -345,8 +345,8 @@ func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) { } } if field != "" { - err = multierr.Append(err, &errorTypes.PipelineError{ - Type: errorTypes.PipelineErrorTypeBadHabit, + err = multierr.Append(err, &pipeline_errors.PipelineError{ + Type: pipeline_errors.PipelineErrorTypeBadHabit, Message: "Set an event filter for all steps or the entire workflow on all items of the `when` block", Data: errors.BadHabitErrorData{ File: config.File, diff --git a/pipeline/frontend/yaml/matrix/matrix.go b/pipeline/frontend/yaml/matrix/matrix.go index a48ad31d8c2..7616f07fbe6 100644 --- a/pipeline/frontend/yaml/matrix/matrix.go +++ b/pipeline/frontend/yaml/matrix/matrix.go @@ -19,7 +19,7 @@ import ( "codeberg.org/6543/xyaml" - errorTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" ) const ( @@ -116,7 +116,7 @@ func parse(raw []byte) (Matrix, error) { Matrix map[string][]string }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} + return nil, &pipeline_errors.PipelineError{Message: err.Error(), Type: pipeline_errors.PipelineErrorTypeCompiler} } return data.Matrix, nil } @@ -129,7 +129,7 @@ func parseList(raw []byte) ([]Axis, error) { }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} + return nil, &pipeline_errors.PipelineError{Message: err.Error(), Type: pipeline_errors.PipelineErrorTypeCompiler} } return data.Matrix.Include, nil } diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go index 60a733964ff..8aba3e5aebd 100644 --- a/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -27,7 +27,7 @@ import ( "golang.org/x/sync/errgroup" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" - pipelineErrors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" ) @@ -122,7 +122,7 @@ func (r *Runtime) Run(runnerCtx context.Context) error { r.started = time.Now().Unix() if err := r.engine.SetupWorkflow(runnerCtx, r.spec, r.taskUUID); err != nil { - var stepErr *pipelineErrors.ErrInvalidWorkflowSetup + var stepErr *pipeline_errors.ErrInvalidWorkflowSetup if errors.As(err, &stepErr) { state := new(State) state.Pipeline.Step = stepErr.Step @@ -147,7 +147,7 @@ func (r *Runtime) Run(runnerCtx context.Context) error { for _, stage := range r.spec.Stages { select { case <-r.ctx.Done(): - return ErrCancel + return pipeline_errors.ErrCancel case err := <-r.execAll(runnerCtx, stage.Steps): if err != nil { r.err = err @@ -244,7 +244,7 @@ func (r *Runtime) execAll(runnerCtx context.Context, steps []*backend.Step) <-ch // normalize context cancel error if errors.Is(err, context.Canceled) { - err = ErrCancel + err = pipeline_errors.ErrCancel } // Return the error after tracing it. @@ -326,7 +326,7 @@ func (r *Runtime) exec(runnerCtx context.Context, step *backend.Step, setupWg *s waitState, err := r.engine.WaitStep(r.ctx, step, r.taskUUID) //nolint:contextcheck if err != nil { if errors.Is(err, context.Canceled) { - waitState.Error = ErrCancel + waitState.Error = pipeline_errors.ErrCancel } else { return nil, err } @@ -343,16 +343,16 @@ func (r *Runtime) exec(runnerCtx context.Context, step *backend.Step, setupWg *s // we handle cancel case if ctxErr := r.ctx.Err(); ctxErr != nil && errors.Is(ctxErr, context.Canceled) { - waitState.Error = ErrCancel + waitState.Error = pipeline_errors.ErrCancel } if waitState.OOMKilled { - return waitState, &OomError{ + return waitState, &pipeline_errors.OomError{ UUID: step.UUID, Code: waitState.ExitCode, } } else if waitState.ExitCode != 0 { - return waitState, &ExitError{ + return waitState, &pipeline_errors.ExitError{ UUID: step.UUID, Code: waitState.ExitCode, } diff --git a/server/model/pipeline.go b/server/model/pipeline.go index 6dbae5008ad..bc9902e5ba3 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -16,45 +16,45 @@ package model import ( - "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" + pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" ) type Pipeline struct { - ID int64 `json:"id" xorm:"pk autoincr 'id'"` - RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'repo_id'"` - Number int64 `json:"number" xorm:"UNIQUE(s) 'number'"` - Author string `json:"author" xorm:"INDEX 'author'"` - Parent int64 `json:"parent" xorm:"parent"` - Event WebhookEvent `json:"event" xorm:"event"` - EventReason []string `json:"event_reason" xorm:"json 'event_reason'"` - Status StatusValue `json:"status" xorm:"INDEX 'status'"` - Errors []*types.PipelineError `json:"errors" xorm:"json 'errors'"` - Created int64 `json:"created" xorm:"'created' NOT NULL DEFAULT 0 created"` - Updated int64 `json:"updated" xorm:"'updated' NOT NULL DEFAULT 0 updated"` - Started int64 `json:"started" xorm:"started"` - Finished int64 `json:"finished" xorm:"finished"` - DeployTo string `json:"deploy_to" xorm:"deploy"` - DeployTask string `json:"deploy_task" xorm:"deploy_task"` - Commit string `json:"commit" xorm:"commit"` - Branch string `json:"branch" xorm:"branch"` - Ref string `json:"ref" xorm:"ref"` - Refspec string `json:"refspec" xorm:"refspec"` - Title string `json:"title" xorm:"title"` - Message string `json:"message" xorm:"TEXT 'message'"` - Timestamp int64 `json:"timestamp" xorm:"'timestamp'"` - Sender string `json:"sender" xorm:"sender"` // uses reported user for webhooks and name of cron for cron pipelines - Avatar string `json:"author_avatar" xorm:"varchar(500) avatar"` - Email string `json:"author_email" xorm:"varchar(500) email"` - ForgeURL string `json:"forge_url" xorm:"forge_url"` - Reviewer string `json:"reviewed_by" xorm:"reviewer"` - Reviewed int64 `json:"reviewed" xorm:"reviewed"` - Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` - ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` - AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` - PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` - PullRequestMilestone string `json:"pr_milestone,omitempty" xorm:"pr_milestone"` - IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` - FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"` + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'repo_id'"` + Number int64 `json:"number" xorm:"UNIQUE(s) 'number'"` + Author string `json:"author" xorm:"INDEX 'author'"` + Parent int64 `json:"parent" xorm:"parent"` + Event WebhookEvent `json:"event" xorm:"event"` + EventReason []string `json:"event_reason" xorm:"json 'event_reason'"` + Status StatusValue `json:"status" xorm:"INDEX 'status'"` + Errors []*pipeline_errors.PipelineError `json:"errors" xorm:"json 'errors'"` + Created int64 `json:"created" xorm:"'created' NOT NULL DEFAULT 0 created"` + Updated int64 `json:"updated" xorm:"'updated' NOT NULL DEFAULT 0 updated"` + Started int64 `json:"started" xorm:"started"` + Finished int64 `json:"finished" xorm:"finished"` + DeployTo string `json:"deploy_to" xorm:"deploy"` + DeployTask string `json:"deploy_task" xorm:"deploy_task"` + Commit string `json:"commit" xorm:"commit"` + Branch string `json:"branch" xorm:"branch"` + Ref string `json:"ref" xorm:"ref"` + Refspec string `json:"refspec" xorm:"refspec"` + Title string `json:"title" xorm:"title"` + Message string `json:"message" xorm:"TEXT 'message'"` + Timestamp int64 `json:"timestamp" xorm:"'timestamp'"` + Sender string `json:"sender" xorm:"sender"` // uses reported user for webhooks and name of cron for cron pipelines + Avatar string `json:"author_avatar" xorm:"varchar(500) avatar"` + Email string `json:"author_email" xorm:"varchar(500) email"` + ForgeURL string `json:"forge_url" xorm:"forge_url"` + Reviewer string `json:"reviewed_by" xorm:"reviewer"` + Reviewed int64 `json:"reviewed" xorm:"reviewed"` + Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` + ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` + AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` + PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` + PullRequestMilestone string `json:"pr_milestone,omitempty" xorm:"pr_milestone"` + IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` + FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"` } // @name Pipeline // TableName return database table name for xorm. diff --git a/server/pipeline/stepbuilder/step_builder.go b/server/pipeline/stepbuilder/step_builder.go index ab690ee1f95..f98ca4f7491 100644 --- a/server/pipeline/stepbuilder/step_builder.go +++ b/server/pipeline/stepbuilder/step_builder.go @@ -29,7 +29,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" - errorTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors/types" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/compiler" @@ -139,7 +138,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A // parse yaml pipeline parsed, err := yaml.ParseString(substituted) if err != nil { - return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} + return nil, &pipeline_errors.PipelineError{Message: err.Error(), Type: pipeline_errors.PipelineErrorTypeCompiler} } // lint pipeline From a2d7a107846e7acc5e3d5b6062184eb78ecce4dc Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 12:19:53 +0100 Subject: [PATCH 11/20] swaggo does not understand import alias!!! https://github.com/swaggo/swag/issues/2016 --- cmd/server/openapi/docs.go | 94 +++++++++++++++++++------------------- server/model/pipeline.go | 72 ++++++++++++++--------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index 0d5539279e2..559e802f3fd 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -5003,7 +5003,7 @@ const docTemplate = `{ "errors": { "type": "array", "items": { - "$ref": "#/definitions/types.PipelineError" + "$ref": "#/definitions/errors.PipelineError" } }, "event": { @@ -5722,6 +5722,52 @@ const docTemplate = `{ "EventManual" ] }, + "errors.PipelineError": { + "type": "object", + "properties": { + "data": {}, + "is_warning": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/errors.PipelineErrorType" + } + } + }, + "errors.PipelineErrorType": { + "type": "string", + "enum": [ + "linter", + "deprecation", + "compiler", + "generic", + "bad_habit" + ], + "x-enum-comments": { + "PipelineErrorTypeBadHabit": "some bad-habit error", + "PipelineErrorTypeCompiler": "some error with the config semantics", + "PipelineErrorTypeDeprecation": "using some deprecated feature", + "PipelineErrorTypeGeneric": "some generic error", + "PipelineErrorTypeLinter": "some error with the config syntax" + }, + "x-enum-descriptions": [ + "some error with the config syntax", + "using some deprecated feature", + "some error with the config semantics", + "some generic error", + "some bad-habit error" + ], + "x-enum-varnames": [ + "PipelineErrorTypeLinter", + "PipelineErrorTypeDeprecation", + "PipelineErrorTypeCompiler", + "PipelineErrorTypeGeneric", + "PipelineErrorTypeBadHabit" + ] + }, "metadata.Author": { "type": "object", "properties": { @@ -6142,52 +6188,6 @@ const docTemplate = `{ "$ref": "#/definitions/StatusValue" } } - }, - "types.PipelineError": { - "type": "object", - "properties": { - "data": {}, - "is_warning": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/types.PipelineErrorType" - } - } - }, - "types.PipelineErrorType": { - "type": "string", - "enum": [ - "linter", - "deprecation", - "compiler", - "generic", - "bad_habit" - ], - "x-enum-comments": { - "PipelineErrorTypeBadHabit": "some bad-habit error", - "PipelineErrorTypeCompiler": "some error with the config semantics", - "PipelineErrorTypeDeprecation": "using some deprecated feature", - "PipelineErrorTypeGeneric": "some generic error", - "PipelineErrorTypeLinter": "some error with the config syntax" - }, - "x-enum-descriptions": [ - "some error with the config syntax", - "using some deprecated feature", - "some error with the config semantics", - "some generic error", - "some bad-habit error" - ], - "x-enum-varnames": [ - "PipelineErrorTypeLinter", - "PipelineErrorTypeDeprecation", - "PipelineErrorTypeCompiler", - "PipelineErrorTypeGeneric", - "PipelineErrorTypeBadHabit" - ] } } }` diff --git a/server/model/pipeline.go b/server/model/pipeline.go index bc9902e5ba3..eb332ce0b03 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -16,45 +16,45 @@ package model import ( - pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" ) type Pipeline struct { - ID int64 `json:"id" xorm:"pk autoincr 'id'"` - RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'repo_id'"` - Number int64 `json:"number" xorm:"UNIQUE(s) 'number'"` - Author string `json:"author" xorm:"INDEX 'author'"` - Parent int64 `json:"parent" xorm:"parent"` - Event WebhookEvent `json:"event" xorm:"event"` - EventReason []string `json:"event_reason" xorm:"json 'event_reason'"` - Status StatusValue `json:"status" xorm:"INDEX 'status'"` - Errors []*pipeline_errors.PipelineError `json:"errors" xorm:"json 'errors'"` - Created int64 `json:"created" xorm:"'created' NOT NULL DEFAULT 0 created"` - Updated int64 `json:"updated" xorm:"'updated' NOT NULL DEFAULT 0 updated"` - Started int64 `json:"started" xorm:"started"` - Finished int64 `json:"finished" xorm:"finished"` - DeployTo string `json:"deploy_to" xorm:"deploy"` - DeployTask string `json:"deploy_task" xorm:"deploy_task"` - Commit string `json:"commit" xorm:"commit"` - Branch string `json:"branch" xorm:"branch"` - Ref string `json:"ref" xorm:"ref"` - Refspec string `json:"refspec" xorm:"refspec"` - Title string `json:"title" xorm:"title"` - Message string `json:"message" xorm:"TEXT 'message'"` - Timestamp int64 `json:"timestamp" xorm:"'timestamp'"` - Sender string `json:"sender" xorm:"sender"` // uses reported user for webhooks and name of cron for cron pipelines - Avatar string `json:"author_avatar" xorm:"varchar(500) avatar"` - Email string `json:"author_email" xorm:"varchar(500) email"` - ForgeURL string `json:"forge_url" xorm:"forge_url"` - Reviewer string `json:"reviewed_by" xorm:"reviewer"` - Reviewed int64 `json:"reviewed" xorm:"reviewed"` - Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` - ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` - AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` - PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` - PullRequestMilestone string `json:"pr_milestone,omitempty" xorm:"pr_milestone"` - IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` - FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"` + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'repo_id'"` + Number int64 `json:"number" xorm:"UNIQUE(s) 'number'"` + Author string `json:"author" xorm:"INDEX 'author'"` + Parent int64 `json:"parent" xorm:"parent"` + Event WebhookEvent `json:"event" xorm:"event"` + EventReason []string `json:"event_reason" xorm:"json 'event_reason'"` + Status StatusValue `json:"status" xorm:"INDEX 'status'"` + Errors []*errors.PipelineError `json:"errors" xorm:"json 'errors'"` + Created int64 `json:"created" xorm:"'created' NOT NULL DEFAULT 0 created"` + Updated int64 `json:"updated" xorm:"'updated' NOT NULL DEFAULT 0 updated"` + Started int64 `json:"started" xorm:"started"` + Finished int64 `json:"finished" xorm:"finished"` + DeployTo string `json:"deploy_to" xorm:"deploy"` + DeployTask string `json:"deploy_task" xorm:"deploy_task"` + Commit string `json:"commit" xorm:"commit"` + Branch string `json:"branch" xorm:"branch"` + Ref string `json:"ref" xorm:"ref"` + Refspec string `json:"refspec" xorm:"refspec"` + Title string `json:"title" xorm:"title"` + Message string `json:"message" xorm:"TEXT 'message'"` + Timestamp int64 `json:"timestamp" xorm:"'timestamp'"` + Sender string `json:"sender" xorm:"sender"` // uses reported user for webhooks and name of cron for cron pipelines + Avatar string `json:"author_avatar" xorm:"varchar(500) avatar"` + Email string `json:"author_email" xorm:"varchar(500) email"` + ForgeURL string `json:"forge_url" xorm:"forge_url"` + Reviewer string `json:"reviewed_by" xorm:"reviewer"` + Reviewed int64 `json:"reviewed" xorm:"reviewed"` + Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` + ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` + AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` + PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` + PullRequestMilestone string `json:"pr_milestone,omitempty" xorm:"pr_milestone"` + IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` + FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"` } // @name Pipeline // TableName return database table name for xorm. From 6a3127254ff02aa7bd8529ad2f5bec1e4e79b5c8 Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 12:53:32 +0100 Subject: [PATCH 12/20] move pipeline runitme struct into own runtime package --- agent/runner.go | 16 ++-- agent/tracer.go | 3 +- cli/exec/exec.go | 13 ++-- pipeline/{pipeline.go => runtime/executor.go} | 75 ++----------------- pipeline/{ => runtime}/option.go | 7 +- pipeline/runtime/runtime.go | 69 +++++++++++++++++ pipeline/{ => runtime}/shutdown.go | 2 +- pipeline/tracer.go | 10 ++- 8 files changed, 102 insertions(+), 93 deletions(-) rename pipeline/{pipeline.go => runtime/executor.go} (81%) rename pipeline/{ => runtime}/option.go (90%) create mode 100644 pipeline/runtime/runtime.go rename pipeline/{ => runtime}/shutdown.go (98%) diff --git a/agent/runner.go b/agent/runner.go index bf914595a93..900221588cf 100644 --- a/agent/runner.go +++ b/agent/runner.go @@ -25,9 +25,9 @@ import ( "github.com/rs/zerolog/log" "google.golang.org/grpc/metadata" - "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" + pipeline_runtime "go.woodpecker-ci.org/woodpecker/v3/pipeline/runtime" "go.woodpecker-ci.org/woodpecker/v3/rpc" "go.woodpecker-ci.org/woodpecker/v3/shared/constant" "go.woodpecker-ci.org/woodpecker/v3/shared/utils" @@ -154,14 +154,14 @@ func (r *Runner) Run(runnerCtx, shutdownCtx context.Context) error { var uploads sync.WaitGroup // Run pipeline - err = pipeline.New( + err = pipeline_runtime.New( workflow.Config, - pipeline.WithContext(workflowCtx), - pipeline.WithTaskUUID(fmt.Sprint(workflow.ID)), - pipeline.WithLogger(r.createLogger(logger, &uploads, workflow)), - pipeline.WithTracer(r.createTracer(ctxMeta, &uploads, logger, workflow)), - pipeline.WithBackend(*r.backend), - pipeline.WithDescription(map[string]string{ + pipeline_runtime.WithContext(workflowCtx), + pipeline_runtime.WithTaskUUID(fmt.Sprint(workflow.ID)), + pipeline_runtime.WithLogger(r.createLogger(logger, &uploads, workflow)), + pipeline_runtime.WithTracer(r.createTracer(ctxMeta, &uploads, logger, workflow)), + pipeline_runtime.WithBackend(*r.backend), + pipeline_runtime.WithDescription(map[string]string{ "workflow_id": workflow.ID, "repo": repoName, "pipeline_number": pipelineNumber, diff --git a/agent/tracer.go b/agent/tracer.go index b5f7907b724..3713382508a 100644 --- a/agent/tracer.go +++ b/agent/tracer.go @@ -26,11 +26,12 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/state" "go.woodpecker-ci.org/woodpecker/v3/rpc" ) func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, logger zerolog.Logger, workflow *rpc.Workflow) pipeline.TraceFunc { - return func(state *pipeline.State) error { + return func(state *state.State) error { uploads.Add(1) defer uploads.Done() diff --git a/cli/exec/exec.go b/cli/exec/exec.go index c96c5beb5fd..547601fda9d 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -45,6 +45,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/compiler" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/matrix" + pipeline_runtime "go.woodpecker-ci.org/woodpecker/v3/pipeline/runtime" pipeline_utils "go.woodpecker-ci.org/woodpecker/v3/pipeline/utils" "go.woodpecker-ci.org/woodpecker/v3/shared/constant" "go.woodpecker-ci.org/woodpecker/v3/shared/utils" @@ -318,12 +319,12 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax fmt.Printf("ctrl+c received, terminating current pipeline '%s'\n", confStr) }) - return pipeline.New(compiled, - pipeline.WithContext(pipelineCtx), //nolint:contextcheck - pipeline.WithTracer(pipeline.DefaultTracer), - pipeline.WithLogger(defaultLogger), - pipeline.WithBackend(backendEngine), - pipeline.WithDescription(map[string]string{ + return pipeline_runtime.New(compiled, + pipeline_runtime.WithContext(pipelineCtx), //nolint:contextcheck + pipeline_runtime.WithTracer(pipeline.DefaultTracer), + pipeline_runtime.WithLogger(defaultLogger), + pipeline_runtime.WithBackend(backendEngine), + pipeline_runtime.WithDescription(map[string]string{ "CLI": "exec", }), ).Run(ctx) diff --git a/pipeline/pipeline.go b/pipeline/runtime/executor.go similarity index 81% rename from pipeline/pipeline.go rename to pipeline/runtime/executor.go index 8aba3e5aebd..cc480341a88 100644 --- a/pipeline/pipeline.go +++ b/pipeline/runtime/executor.go @@ -1,4 +1,4 @@ -// Copyright 2023 Woodpecker Authors +// Copyright 2026 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pipeline +package runtime import ( "context" @@ -21,79 +21,14 @@ import ( "sync" "time" - "github.com/oklog/ulid/v2" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/state" ) -// TODO: move runtime into "runtime" subpackage - -type ( - // State defines the pipeline and process state. - State struct { - // Global state of the pipeline. - Pipeline struct { - // Pipeline time started - Started int64 `json:"time"` - // Current pipeline step - Step *backend.Step `json:"step"` - // Current pipeline error state - Error error `json:"error"` - } - - // Current process state. - Process backend.State - } -) - -// Runtime represents a workflow state executed by a specific backend. -// Each workflow gets its own state configuration at runtime. -type Runtime struct { - err error - spec *backend.Config - engine backend.Backend - started int64 - - // The context a workflow is being executed with. - // All normal (non cleanup) operations must use this. - // Cleanup operations should use the runnerCtx passed to Run() - ctx context.Context - - tracer Tracer - logger Logger - - taskUUID string - - Description map[string]string // The runtime descriptors. -} - -// New returns a new runtime using the specified runtime -// configuration and runtime engine. -func New(spec *backend.Config, opts ...Option) *Runtime { - r := new(Runtime) - r.Description = map[string]string{} - r.spec = spec - r.ctx = context.Background() - r.taskUUID = ulid.Make().String() - for _, opts := range opts { - opts(r) - } - return r -} - -func (r *Runtime) MakeLogger() zerolog.Logger { - logCtx := log.With() - for key, val := range r.Description { - logCtx = logCtx.Str(key, val) - } - return logCtx.Logger() -} - // Run starts the execution of a workflow and waits for it to complete. func (r *Runtime) Run(runnerCtx context.Context) error { logger := r.MakeLogger() @@ -124,7 +59,7 @@ func (r *Runtime) Run(runnerCtx context.Context) error { if err := r.engine.SetupWorkflow(runnerCtx, r.spec, r.taskUUID); err != nil { var stepErr *pipeline_errors.ErrInvalidWorkflowSetup if errors.As(err, &stepErr) { - state := new(State) + state := new(state.State) state.Pipeline.Step = stepErr.Step state.Pipeline.Error = stepErr.Err state.Process = backend.State{ @@ -167,7 +102,7 @@ func (r *Runtime) traceStep(processState *backend.State, err error, step *backen return nil } - state := new(State) + state := new(state.State) state.Pipeline.Started = r.started state.Pipeline.Step = step state.Pipeline.Error = r.err diff --git a/pipeline/option.go b/pipeline/runtime/option.go similarity index 90% rename from pipeline/option.go rename to pipeline/runtime/option.go index 784cfdbbd59..4c409dd6b5b 100644 --- a/pipeline/option.go +++ b/pipeline/runtime/option.go @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pipeline +package runtime import ( "context" + "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" ) @@ -31,14 +32,14 @@ func WithBackend(backend backend.Backend) Option { } // WithLogger returns an option configured with a runtime logger. -func WithLogger(logger Logger) Option { +func WithLogger(logger pipeline.Logger) Option { return func(r *Runtime) { r.logger = logger } } // WithTracer returns an option configured with a runtime tracer. -func WithTracer(tracer Tracer) Option { +func WithTracer(tracer pipeline.Tracer) Option { return func(r *Runtime) { r.tracer = tracer } diff --git a/pipeline/runtime/runtime.go b/pipeline/runtime/runtime.go new file mode 100644 index 00000000000..1c25b0a3f62 --- /dev/null +++ b/pipeline/runtime/runtime.go @@ -0,0 +1,69 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "context" + + "github.com/oklog/ulid/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "go.woodpecker-ci.org/woodpecker/v3/pipeline" + backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" +) + +// Runtime represents a workflow state executed by a specific backend. +// Each workflow gets its own state configuration at runtime. +type Runtime struct { + err error + spec *backend.Config + engine backend.Backend + started int64 + + // The context a workflow is being executed with. + // All normal (non cleanup) operations must use this. + // Cleanup operations should use the runnerCtx passed to Run() + ctx context.Context + + tracer pipeline.Tracer + logger pipeline.Logger + + taskUUID string + + Description map[string]string // The runtime descriptors. +} + +// New returns a new runtime using the specified runtime +// configuration and runtime engine. +func New(spec *backend.Config, opts ...Option) *Runtime { + r := new(Runtime) + r.Description = map[string]string{} + r.spec = spec + r.ctx = context.Background() + r.taskUUID = ulid.Make().String() + for _, opts := range opts { + opts(r) + } + return r +} + +func (r *Runtime) MakeLogger() zerolog.Logger { + logCtx := log.With() + for key, val := range r.Description { + logCtx = logCtx.Str(key, val) + } + return logCtx.Logger() +} diff --git a/pipeline/shutdown.go b/pipeline/runtime/shutdown.go similarity index 98% rename from pipeline/shutdown.go rename to pipeline/runtime/shutdown.go index 9479ab9710f..d53cf1c0d7f 100644 --- a/pipeline/shutdown.go +++ b/pipeline/runtime/shutdown.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pipeline +package runtime import ( "context" diff --git a/pipeline/tracer.go b/pipeline/tracer.go index f48a7c562d3..37f6c3108dc 100644 --- a/pipeline/tracer.go +++ b/pipeline/tracer.go @@ -16,26 +16,28 @@ package pipeline import ( "strconv" + + "go.woodpecker-ci.org/woodpecker/v3/pipeline/state" ) // Tracer handles process tracing. type Tracer interface { - Trace(*State) error + Trace(*state.State) error } // TraceFunc type is an adapter to allow the use of ordinary // functions as a Tracer. -type TraceFunc func(*State) error +type TraceFunc func(*state.State) error // Trace calls f(state). -func (f TraceFunc) Trace(state *State) error { +func (f TraceFunc) Trace(state *state.State) error { return f(state) } // DefaultTracer provides a tracer that updates the CI_ environment // variables to include the correct timestamp and status. // TODO: find either a new home or better name for this. -var DefaultTracer = TraceFunc(func(state *State) error { +var DefaultTracer = TraceFunc(func(state *state.State) error { if state.Process.Exited { return nil } From 7dfafe5d01a4b8229bb79dc7577620037a17583d Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 13:00:19 +0100 Subject: [PATCH 13/20] move engine trace and log stuf in subpackages --- agent/logger.go | 3 ++- agent/tracer.go | 4 ++-- cli/exec/exec.go | 6 ++++-- pipeline/{ => logging}/logger.go | 2 +- pipeline/runtime/option.go | 7 ++++--- pipeline/runtime/runtime.go | 7 ++++--- pipeline/{ => tracing}/tracer.go | 2 +- 7 files changed, 18 insertions(+), 13 deletions(-) rename pipeline/{ => logging}/logger.go (97%) rename pipeline/{ => tracing}/tracer.go (98%) diff --git a/agent/logger.go b/agent/logger.go index 89ced9c7172..528b8830c78 100644 --- a/agent/logger.go +++ b/agent/logger.go @@ -23,11 +23,12 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/agent/log" "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/logging" pipeline_utils "go.woodpecker-ci.org/woodpecker/v3/pipeline/utils" "go.woodpecker-ci.org/woodpecker/v3/rpc" ) -func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, workflow *rpc.Workflow) pipeline.Logger { +func (r *Runner) createLogger(_logger zerolog.Logger, uploads *sync.WaitGroup, workflow *rpc.Workflow) logging.Logger { return func(step *backend.Step, rc io.ReadCloser) error { defer rc.Close() diff --git a/agent/tracer.go b/agent/tracer.go index 3713382508a..df4b0c814fe 100644 --- a/agent/tracer.go +++ b/agent/tracer.go @@ -24,13 +24,13 @@ import ( "github.com/rs/zerolog" - "go.woodpecker-ci.org/woodpecker/v3/pipeline" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/pipeline/state" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/tracing" "go.woodpecker-ci.org/woodpecker/v3/rpc" ) -func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, logger zerolog.Logger, workflow *rpc.Workflow) pipeline.TraceFunc { +func (r *Runner) createTracer(ctxMeta context.Context, uploads *sync.WaitGroup, logger zerolog.Logger, workflow *rpc.Workflow) tracing.TraceFunc { return func(state *state.State) error { uploads.Add(1) defer uploads.Done() diff --git a/cli/exec/exec.go b/cli/exec/exec.go index 547601fda9d..c338f435f60 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -45,7 +45,9 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/compiler" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/matrix" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/logging" pipeline_runtime "go.woodpecker-ci.org/woodpecker/v3/pipeline/runtime" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/tracing" pipeline_utils "go.woodpecker-ci.org/woodpecker/v3/pipeline/utils" "go.woodpecker-ci.org/woodpecker/v3/shared/constant" "go.woodpecker-ci.org/woodpecker/v3/shared/utils" @@ -321,7 +323,7 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax return pipeline_runtime.New(compiled, pipeline_runtime.WithContext(pipelineCtx), //nolint:contextcheck - pipeline_runtime.WithTracer(pipeline.DefaultTracer), + pipeline_runtime.WithTracer(tracing.DefaultTracer), pipeline_runtime.WithLogger(defaultLogger), pipeline_runtime.WithBackend(backendEngine), pipeline_runtime.WithDescription(map[string]string{ @@ -349,7 +351,7 @@ func convertPathForWindows(path string) string { return filepath.ToSlash(path) } -var defaultLogger = pipeline.Logger(func(step *backend_types.Step, rc io.ReadCloser) error { +var defaultLogger = logging.Logger(func(step *backend_types.Step, rc io.ReadCloser) error { logWriter := NewLineWriter(step.Name, step.UUID) return pipeline_utils.CopyLineByLine(logWriter, rc, pipeline.MaxLogLineLength) }) diff --git a/pipeline/logger.go b/pipeline/logging/logger.go similarity index 97% rename from pipeline/logger.go rename to pipeline/logging/logger.go index 765a0333b7b..e7e55f24e65 100644 --- a/pipeline/logger.go +++ b/pipeline/logging/logger.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pipeline +package logging import ( "io" diff --git a/pipeline/runtime/option.go b/pipeline/runtime/option.go index 4c409dd6b5b..f87d4a108a9 100644 --- a/pipeline/runtime/option.go +++ b/pipeline/runtime/option.go @@ -17,8 +17,9 @@ package runtime import ( "context" - "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/logging" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/tracing" ) // Option configures a runtime option. @@ -32,14 +33,14 @@ func WithBackend(backend backend.Backend) Option { } // WithLogger returns an option configured with a runtime logger. -func WithLogger(logger pipeline.Logger) Option { +func WithLogger(logger logging.Logger) Option { return func(r *Runtime) { r.logger = logger } } // WithTracer returns an option configured with a runtime tracer. -func WithTracer(tracer pipeline.Tracer) Option { +func WithTracer(tracer tracing.Tracer) Option { return func(r *Runtime) { r.tracer = tracer } diff --git a/pipeline/runtime/runtime.go b/pipeline/runtime/runtime.go index 1c25b0a3f62..6b849b33545 100644 --- a/pipeline/runtime/runtime.go +++ b/pipeline/runtime/runtime.go @@ -21,8 +21,9 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "go.woodpecker-ci.org/woodpecker/v3/pipeline" backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/logging" + "go.woodpecker-ci.org/woodpecker/v3/pipeline/tracing" ) // Runtime represents a workflow state executed by a specific backend. @@ -38,8 +39,8 @@ type Runtime struct { // Cleanup operations should use the runnerCtx passed to Run() ctx context.Context - tracer pipeline.Tracer - logger pipeline.Logger + tracer tracing.Tracer + logger logging.Logger taskUUID string diff --git a/pipeline/tracer.go b/pipeline/tracing/tracer.go similarity index 98% rename from pipeline/tracer.go rename to pipeline/tracing/tracer.go index 37f6c3108dc..77b9f3a7df4 100644 --- a/pipeline/tracer.go +++ b/pipeline/tracing/tracer.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pipeline +package tracing import ( "strconv" From 669d026005d2ca0db49c2313371646e52d8f779d Mon Sep 17 00:00:00 2001 From: "m.huber" Date: Fri, 6 Feb 2026 13:14:55 +0100 Subject: [PATCH 14/20] ci.rerun() From 28f102b38f4887c1012c4c9b804008b7df1c8697 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sat, 7 Feb 2026 14:50:48 +0100 Subject: [PATCH 15/20] cleanup and fix after merge main --- pipeline/errors/pipeline.go | 54 +++++++++++++++++++++++++++++++++++++ pipeline/state/state.go | 33 +++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 pipeline/errors/pipeline.go create mode 100644 pipeline/state/state.go diff --git a/pipeline/errors/pipeline.go b/pipeline/errors/pipeline.go new file mode 100644 index 00000000000..ec486fbf872 --- /dev/null +++ b/pipeline/errors/pipeline.go @@ -0,0 +1,54 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +import ( + "fmt" + + backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" +) + +type PipelineErrorType string + +const ( + PipelineErrorTypeLinter PipelineErrorType = "linter" // some error with the config syntax + PipelineErrorTypeDeprecation PipelineErrorType = "deprecation" // using some deprecated feature + PipelineErrorTypeCompiler PipelineErrorType = "compiler" // some error with the config semantics + PipelineErrorTypeGeneric PipelineErrorType = "generic" // some generic error + PipelineErrorTypeBadHabit PipelineErrorType = "bad_habit" // some bad-habit error +) + +type PipelineError struct { + Type PipelineErrorType `json:"type"` + Message string `json:"message"` + IsWarning bool `json:"is_warning"` + Data any `json:"data"` +} + +func (e *PipelineError) Error() string { + return fmt.Sprintf("[%s] %s", e.Type, e.Message) +} + +type ErrInvalidWorkflowSetup struct { + Err error + Step *backend.Step +} + +func (e *ErrInvalidWorkflowSetup) Error() string { + if e.Step != nil { + return fmt.Sprintf("error in workflow setup step '%s': %v", e.Step.Name, e.Err) + } + return fmt.Sprintf("error in workflow setup: %v", e.Err) +} diff --git a/pipeline/state/state.go b/pipeline/state/state.go new file mode 100644 index 00000000000..4533d0f0af4 --- /dev/null +++ b/pipeline/state/state.go @@ -0,0 +1,33 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package state + +type ( + // State defines the pipeline and process state. + State struct { + // Global state of the pipeline. + Pipeline struct { + // Pipeline time started + Started int64 `json:"time"` + // Current pipeline step + Step *backend.Step `json:"step"` + // Current pipeline error state + Error error `json:"error"` + } + + // Current process state. + Process backend.State + } +) From 528446ef51406706d03bc8dbb628e91a353b5ece Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 11 Feb 2026 05:26:42 +0100 Subject: [PATCH 16/20] fix import --- pipeline/state/state.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pipeline/state/state.go b/pipeline/state/state.go index 4533d0f0af4..e8e0818b89d 100644 --- a/pipeline/state/state.go +++ b/pipeline/state/state.go @@ -14,6 +14,10 @@ package state +import ( + backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types" +) + type ( // State defines the pipeline and process state. State struct { From aaabd21ac2520435d10ff85fcaeb9f2bde19bdaa Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 12 Feb 2026 00:00:10 +0100 Subject: [PATCH 17/20] fix-lint --- pipeline/frontend/yaml/linter/linter.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pipeline/frontend/yaml/linter/linter.go b/pipeline/frontend/yaml/linter/linter.go index 74da3661bab..0a560dfd37f 100644 --- a/pipeline/frontend/yaml/linter/linter.go +++ b/pipeline/frontend/yaml/linter/linter.go @@ -20,7 +20,6 @@ import ( "codeberg.org/6543/xyaml" "go.uber.org/multierr" - "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" pipeline_errors "go.woodpecker-ci.org/woodpecker/v3/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/linter/schema" "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types" @@ -348,7 +347,7 @@ func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) { err = multierr.Append(err, &pipeline_errors.PipelineError{ Type: pipeline_errors.PipelineErrorTypeBadHabit, Message: "Set an event filter for all steps or the entire workflow on all items of the `when` block", - Data: errors.BadHabitErrorData{ + Data: pipeline_errors.BadHabitErrorData{ File: config.File, Field: field, Docs: "https://woodpecker-ci.org/docs/usage/linter#event-filter-for-all-steps", From 4730c171d95b72444968a2c826866483ae91a11b Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 13 Feb 2026 01:13:52 +0100 Subject: [PATCH 18/20] copiright year --- pipeline/errors/linter.go | 2 +- pipeline/errors/linter_test.go | 2 +- pipeline/errors/pipeline.go | 2 +- pipeline/errors/runtime.go | 2 +- pipeline/runtime/executor.go | 2 +- pipeline/runtime/runtime.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pipeline/errors/linter.go b/pipeline/errors/linter.go index 76293e99765..9bd7d5d3b1b 100644 --- a/pipeline/errors/linter.go +++ b/pipeline/errors/linter.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pipeline/errors/linter_test.go b/pipeline/errors/linter_test.go index 4eefe0371f1..df6a72bec4e 100644 --- a/pipeline/errors/linter_test.go +++ b/pipeline/errors/linter_test.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pipeline/errors/pipeline.go b/pipeline/errors/pipeline.go index ec486fbf872..ad8351e0752 100644 --- a/pipeline/errors/pipeline.go +++ b/pipeline/errors/pipeline.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2024 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pipeline/errors/runtime.go b/pipeline/errors/runtime.go index 3b7a29a46c0..1b7e2661e94 100644 --- a/pipeline/errors/runtime.go +++ b/pipeline/errors/runtime.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pipeline/runtime/executor.go b/pipeline/runtime/executor.go index 07a15d2e52a..edf1b0d195c 100644 --- a/pipeline/runtime/executor.go +++ b/pipeline/runtime/executor.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pipeline/runtime/runtime.go b/pipeline/runtime/runtime.go index 6b849b33545..5fb197b4ab5 100644 --- a/pipeline/runtime/runtime.go +++ b/pipeline/runtime/runtime.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From a21a236c052b5bcc033ed41c04bf6538a69088c3 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 13 Feb 2026 01:14:35 +0100 Subject: [PATCH 19/20] Update pipeline/state/state.go --- pipeline/state/state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/state/state.go b/pipeline/state/state.go index e8e0818b89d..6a496700ee7 100644 --- a/pipeline/state/state.go +++ b/pipeline/state/state.go @@ -1,4 +1,4 @@ -// Copyright 2026 Woodpecker Authors +// Copyright 2023 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 05b64087d9e9a1f660ef2bf04a8e1ee3d317771e Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 13 Feb 2026 01:16:11 +0100 Subject: [PATCH 20/20] Apply suggestions from code review --- pipeline/errors/runtime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/errors/runtime.go b/pipeline/errors/runtime.go index 1b7e2661e94..774fc79cbbf 100644 --- a/pipeline/errors/runtime.go +++ b/pipeline/errors/runtime.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS,