diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index f3508f1f5e..3130d57e9c 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -15,6 +15,7 @@ package compiler import ( + "errors" "fmt" "maps" "path" @@ -221,6 +222,11 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er } // add pipeline steps + stepNames := make(map[string]struct{}, len(conf.Steps.ContainerList)) + for _, container := range conf.Steps.ContainerList { + stepNames[container.Name] = struct{}{} + } + steps := make([]*dagCompilerStep, 0, len(conf.Steps.ContainerList)) for pos, container := range conf.Steps.ContainerList { // Skip if local and should not run local @@ -259,6 +265,15 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er // generate stages out of steps stepStages, err := newDAGCompiler(steps).compile() if err != nil { + // If the missing dep exists in the config but isn't in the surviving + // step list, it was filtered out by its 'when' conditions. Surface a + // more actionable error. + var missingDepErr *ErrStepMissingDependency + if errors.As(err, &missingDepErr) { + if _, inConfig := stepNames[missingDepErr.dep]; inConfig { + return nil, &ErrStepFilteredDependency{name: missingDepErr.name, dep: missingDepErr.dep} + } + } return nil, err } diff --git a/pipeline/frontend/yaml/compiler/compiler_test.go b/pipeline/frontend/yaml/compiler/compiler_test.go index b6dd3e5f13..28d571e034 100644 --- a/pipeline/frontend/yaml/compiler/compiler_test.go +++ b/pipeline/frontend/yaml/compiler/compiler_test.go @@ -298,6 +298,25 @@ func TestCompilerCompile(t *testing.T) { backConf: nil, expectedErr: "step 'dummy' depends on unknown step 'not exist'", }, + { + name: "workflow with step depending on filtered-out step", + fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{ + { + Name: "build", + Image: "bash", + When: constraint.When{Constraints: []constraint.Constraint{{ + Event: yaml_base_types.StringOrSlice{"tag"}, + }}}, + }, + { + Name: "deploy", + Image: "bash", + DependsOn: constraint.DependsOn{{Name: "build"}}, + }, + }}}, + backConf: nil, + expectedErr: "step 'deploy' depends on step 'build' which is filtered out by its conditions", + }, } for _, test := range tests { diff --git a/pipeline/frontend/yaml/compiler/errors.go b/pipeline/frontend/yaml/compiler/errors.go index c567d16696..b9dc75f932 100644 --- a/pipeline/frontend/yaml/compiler/errors.go +++ b/pipeline/frontend/yaml/compiler/errors.go @@ -43,6 +43,20 @@ func (*ErrStepMissingDependency) Is(target error) bool { return ok } +type ErrStepFilteredDependency struct { + name, + dep string +} + +func (err *ErrStepFilteredDependency) Error() string { + return fmt.Sprintf("step '%s' depends on step '%s' which is filtered out by its conditions", err.name, err.dep) +} + +func (*ErrStepFilteredDependency) Is(target error) bool { + _, ok := target.(*ErrStepFilteredDependency) + return ok +} + type ErrStepDependencyCycle struct { path []string }