diff --git a/server/api/badge.go b/server/api/badge.go index 88188fff1..6d5a0e36f 100644 --- a/server/api/badge.go +++ b/server/api/badge.go @@ -30,6 +30,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/badges" "go.woodpecker-ci.org/woodpecker/v3/server/ccmenu" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/pipeline" "go.woodpecker-ci.org/woodpecker/v3/server/store" "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) @@ -96,13 +97,13 @@ func GetBadge(c *gin.Context) { name := "pipeline" var status *model.StatusValue = nil - pipeline, err := _store.GetPipelineBadge(repo, branch, events) + pl, err := _store.GetPipelineBadge(repo, branch, events) if err != nil { if !errors.Is(err, types.RecordNotExist) { log.Warn().Err(err).Msg("could not get last pipeline for badge") } } else { - status = &pipeline.Status + status = &pl.Status } // we serve an SVG, so set content type appropriately. @@ -114,13 +115,16 @@ func GetBadge(c *gin.Context) { name = workflowName status = nil - workflows, err := _store.WorkflowGetTree(pipeline) + workflows, err := _store.WorkflowGetTree(pl) if err == nil { for _, wf := range workflows { if wf.Name == workflowName { stepName := c.Query("step") if len(stepName) == 0 { - if status == nil || wf.Failing() { + if status != nil { + merged := pipeline.MergeStatusValues(*status, wf.State) + status = &merged + } else { status = &wf.State } continue @@ -129,7 +133,10 @@ func GetBadge(c *gin.Context) { name = workflowName + ": " + stepName for _, s := range wf.Children { if s.Name == stepName { - if status == nil || s.Failing() { + if status != nil { + merged := pipeline.MergeStatusValues(*status, s.State) + status = &merged + } else { status = &s.State } } diff --git a/server/model/workflow.go b/server/model/workflow.go index e6029ab69..53a21a2dc 100644 --- a/server/model/workflow.go +++ b/server/model/workflow.go @@ -57,30 +57,3 @@ func IsThereRunningStage(workflows []*Workflow) bool { } return false } - -// PipelineStatus determine pipeline status based on corresponding workflow list. -func PipelineStatus(workflows []*Workflow) StatusValue { - status := StatusSuccess - - for _, p := range workflows { - if p.Failing() { - status = p.State - } - } - - return status -} - -// WorkflowStatus determine workflow status based on corresponding step list. -func WorkflowStatus(steps []*Step) StatusValue { - status := StatusSuccess - - for _, p := range steps { - if p.Failing() { - status = p.State - break - } - } - - return status -} diff --git a/server/pipeline/pipeline_status.go b/server/pipeline/pipeline_status.go index 259b557f2..86bf7183b 100644 --- a/server/pipeline/pipeline_status.go +++ b/server/pipeline/pipeline_status.go @@ -23,6 +23,17 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/store" ) +// PipelineStatus determine pipeline status based on corresponding workflow list. +func PipelineStatus(workflows []*model.Workflow) model.StatusValue { + status := model.StatusSuccess + + for _, p := range workflows { + status = MergeStatusValues(status, p.State) + } + + return status +} + func UpdateToStatusRunning(store store.Store, pipeline model.Pipeline, started int64) (*model.Pipeline, error) { pipeline.Status = model.StatusRunning pipeline.Started = started diff --git a/server/pipeline/status.go b/server/pipeline/status.go new file mode 100644 index 000000000..4efdf1c13 --- /dev/null +++ b/server/pipeline/status.go @@ -0,0 +1,55 @@ +// 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 pipeline + +import "go.woodpecker-ci.org/woodpecker/v3/server/model" + +// list of statuses by their priority. Most important is on top. +var statusPriorityOrder = []model.StatusValue{ + // blocked, declined and created cannot appear in the + // same workflow/pipeline at the same time + model.StatusDeclined, + model.StatusBlocked, + model.StatusCreated, + + // errors have highest priority. + model.StatusError, + + // skipped and killed cannot appear together with running/pending. + model.StatusKilled, + model.StatusSkipped, + + // running states + model.StatusRunning, + model.StatusPending, + + // finished states + model.StatusFailure, + model.StatusSuccess, +} + +var priorityMap map[model.StatusValue]int = buildPriorityMap() + +func buildPriorityMap() map[model.StatusValue]int { + m := map[model.StatusValue]int{} + for i, s := range statusPriorityOrder { + m[s] = i + } + return m +} + +func MergeStatusValues(s, t model.StatusValue) model.StatusValue { + return statusPriorityOrder[min(priorityMap[s], priorityMap[t])] +} diff --git a/server/pipeline/status_test.go b/server/pipeline/status_test.go new file mode 100644 index 000000000..18171d281 --- /dev/null +++ b/server/pipeline/status_test.go @@ -0,0 +1,76 @@ +// 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 pipeline + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v3/server/model" +) + +func TestStatusValueMerge(t *testing.T) { + tests := []struct { + s model.StatusValue + t model.StatusValue + e model.StatusValue + }{ + { + s: model.StatusSuccess, + t: model.StatusSkipped, + e: model.StatusSkipped, + }, + { + s: model.StatusSuccess, + t: model.StatusSuccess, + e: model.StatusSuccess, + }, + { + s: model.StatusFailure, + t: model.StatusSuccess, + e: model.StatusFailure, + }, + { + s: model.StatusRunning, + t: model.StatusSuccess, + e: model.StatusRunning, + }, + { + s: model.StatusRunning, + t: model.StatusFailure, + e: model.StatusRunning, + }, + { + s: model.StatusFailure, + t: model.StatusKilled, + e: model.StatusKilled, + }, + { + s: model.StatusSkipped, + t: model.StatusKilled, + e: model.StatusKilled, + }, + { + s: model.StatusSkipped, + t: model.StatusSkipped, + e: model.StatusSkipped, + }, + } + for _, tt := range tests { + assert.Equal(t, tt.e, MergeStatusValues(tt.s, tt.t)) + assert.Equal(t, tt.e, MergeStatusValues(tt.t, tt.s)) + } +} diff --git a/server/pipeline/workflow_status.go b/server/pipeline/workflow_status.go index c65982e49..6174aca13 100644 --- a/server/pipeline/workflow_status.go +++ b/server/pipeline/workflow_status.go @@ -20,6 +20,17 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/store" ) +// WorkflowStatus determine workflow status based on corresponding step list. +func WorkflowStatus(steps []*model.Step) model.StatusValue { + status := model.StatusSuccess + + for _, p := range steps { + status = MergeStatusValues(status, p.State) + } + + return status +} + func UpdateWorkflowStatusToRunning(store store.Store, workflow model.Workflow, state rpc.WorkflowState) (*model.Workflow, error) { workflow.Started = state.Started workflow.State = model.StatusRunning @@ -37,7 +48,7 @@ func UpdateWorkflowStatusToDone(store store.Store, workflow model.Workflow, stat if state.Started == 0 { workflow.State = model.StatusSkipped } else { - workflow.State = model.WorkflowStatus(workflow.Children) + workflow.State = WorkflowStatus(workflow.Children) } if workflow.Error != "" { workflow.State = model.StatusFailure diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go index 5ff78e275..6fa97735d 100644 --- a/server/rpc/rpc.go +++ b/server/rpc/rpc.go @@ -369,7 +369,7 @@ func (s *RPC) Done(c context.Context, strWorkflowID string, state rpc.WorkflowSt s.completeChildrenIfParentCompleted(workflow) if !model.IsThereRunningStage(currentPipeline.Workflows) { - if currentPipeline, err = pipeline.UpdateStatusToDone(s.store, *currentPipeline, model.PipelineStatus(currentPipeline.Workflows), workflow.Finished); err != nil { + if currentPipeline, err = pipeline.UpdateStatusToDone(s.store, *currentPipeline, pipeline.PipelineStatus(currentPipeline.Workflows), workflow.Finished); err != nil { logger.Error().Err(err).Msgf("pipeline.UpdateStatusToDone: cannot update workflows final state") } }