From cf8aea332b6062c23725ec7ea2951a39f7bfe1a0 Mon Sep 17 00:00:00 2001 From: Easton Crupper <65553218+ecrupper@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:44:51 -0600 Subject: [PATCH] fix(compiler): surface template warnings and StringSliceMap YAML errors (#1251) * fix(compiler): treat top level anchors like nested anchors * fix(compiler): surface template warnings and StringSliceMap YAML errors * brackets surrounding template name for clarity --- compiler/engine.go | 4 +- compiler/native/compile.go | 26 ++++-- compiler/native/expand.go | 43 +++++---- compiler/native/expand_test.go | 134 +++++++++++++++++++++++---- compiler/native/parse.go | 2 +- compiler/template/native/render.go | 4 +- compiler/template/starlark/render.go | 4 +- compiler/types/raw/map.go | 3 +- internal/yaml.go | 25 +++-- internal/yaml_test.go | 20 ++-- 10 files changed, 201 insertions(+), 64 deletions(-) diff --git a/compiler/engine.go b/compiler/engine.go index fd38599ef..60d4e9e36 100644 --- a/compiler/engine.go +++ b/compiler/engine.go @@ -74,10 +74,10 @@ type Engine interface { // ExpandStages defines a function that injects the template // for each templated step in every stage in a yaml configuration. - ExpandStages(context.Context, *yaml.Build, map[string]*yaml.Template, *pipeline.RuleData) (*yaml.Build, error) + ExpandStages(context.Context, *yaml.Build, map[string]*yaml.Template, *pipeline.RuleData, []string) (*yaml.Build, []string, error) // ExpandSteps defines a function that injects the template // for each templated step in a yaml configuration with the provided template depth. - ExpandSteps(context.Context, *yaml.Build, map[string]*yaml.Template, *pipeline.RuleData, int) (*yaml.Build, error) + ExpandSteps(context.Context, *yaml.Build, map[string]*yaml.Template, *pipeline.RuleData, []string, int) (*yaml.Build, []string, error) // Init Compiler Interface Functions diff --git a/compiler/native/compile.go b/compiler/native/compile.go index e8152d81b..bf018b1ae 100644 --- a/compiler/native/compile.go +++ b/compiler/native/compile.go @@ -155,11 +155,13 @@ func (c *client) CompileLite(ctx context.Context, v interface{}, ruleData *pipel switch { case len(p.Stages) > 0: // inject the templates into the steps - p, err = c.ExpandStages(ctx, p, templates, ruleData) + p, warnings, err = c.ExpandStages(ctx, p, templates, ruleData, _pipeline.GetWarnings()) if err != nil { return nil, _pipeline, err } + _pipeline.SetWarnings(warnings) + if substitute { // inject the substituted environment variables into the steps p.Stages, err = c.SubstituteStages(p.Stages) @@ -193,11 +195,13 @@ func (c *client) CompileLite(ctx context.Context, v interface{}, ruleData *pipel case len(p.Steps) > 0: // inject the templates into the steps - p, err = c.ExpandSteps(ctx, p, templates, ruleData, c.GetTemplateDepth()) + p, warnings, err = c.ExpandSteps(ctx, p, templates, ruleData, _pipeline.GetWarnings(), c.GetTemplateDepth()) if err != nil { return nil, _pipeline, err } + _pipeline.SetWarnings(warnings) + if substitute { // inject the substituted environment variables into the steps p.Steps, err = c.SubstituteSteps(p.Steps) @@ -334,7 +338,10 @@ func (c *client) compileInline(ctx context.Context, p *yaml.Build, depth int) (* // compileSteps executes the workflow for converting a YAML pipeline into an executable struct. func (c *client) compileSteps(ctx context.Context, p *yaml.Build, _pipeline *api.Pipeline, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*pipeline.Build, *api.Pipeline, error) { - var err error + var ( + warnings []string + err error + ) // check if the pipeline disabled the clone if p.Metadata.Clone == nil || *p.Metadata.Clone { @@ -358,11 +365,13 @@ func (c *client) compileSteps(ctx context.Context, p *yaml.Build, _pipeline *api } // inject the templates into the steps - p, err = c.ExpandSteps(ctx, p, tmpls, r, c.GetTemplateDepth()) + p, warnings, err = c.ExpandSteps(ctx, p, tmpls, r, _pipeline.GetWarnings(), c.GetTemplateDepth()) if err != nil { return nil, _pipeline, err } + _pipeline.SetWarnings(warnings) + if c.ModificationService.Endpoint != "" { // send config to external endpoint for modification // @@ -437,7 +446,10 @@ func (c *client) compileSteps(ctx context.Context, p *yaml.Build, _pipeline *api // compileStages executes the workflow for converting a YAML pipeline into an executable struct. func (c *client) compileStages(ctx context.Context, p *yaml.Build, _pipeline *api.Pipeline, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*pipeline.Build, *api.Pipeline, error) { - var err error + var ( + warnings []string + err error + ) // check if the pipeline disabled the clone if p.Metadata.Clone == nil || *p.Metadata.Clone { @@ -455,11 +467,13 @@ func (c *client) compileStages(ctx context.Context, p *yaml.Build, _pipeline *ap } // inject the templates into the stages - p, err = c.ExpandStages(ctx, p, tmpls, r) + p, warnings, err = c.ExpandStages(ctx, p, tmpls, r, _pipeline.GetWarnings()) if err != nil { return nil, _pipeline, err } + _pipeline.SetWarnings(warnings) + if c.ModificationService.Endpoint != "" { // send config to external endpoint for modification // diff --git a/compiler/native/expand.go b/compiler/native/expand.go index df3de903b..2479e6ff1 100644 --- a/compiler/native/expand.go +++ b/compiler/native/expand.go @@ -21,17 +21,22 @@ import ( // ExpandStages injects the template for each // templated step in every stage in a yaml configuration. -func (c *client) ExpandStages(ctx context.Context, s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*yaml.Build, error) { +func (c *client) ExpandStages(ctx context.Context, s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData, warnings []string) (*yaml.Build, []string, error) { + var ( + p *yaml.Build + err error + ) + if len(tmpls) == 0 { - return s, nil + return s, warnings, nil } // iterate through all stages for _, stage := range s.Stages { // inject the templates into the steps for the stage - p, err := c.ExpandSteps(ctx, &yaml.Build{Steps: stage.Steps, Secrets: s.Secrets, Services: s.Services, Environment: s.Environment}, tmpls, r, c.GetTemplateDepth()) + p, warnings, err = c.ExpandSteps(ctx, &yaml.Build{Steps: stage.Steps, Secrets: s.Secrets, Services: s.Services, Environment: s.Environment}, tmpls, r, warnings, c.GetTemplateDepth()) if err != nil { - return nil, err + return nil, warnings, err } stage.Steps = p.Steps @@ -40,23 +45,23 @@ func (c *client) ExpandStages(ctx context.Context, s *yaml.Build, tmpls map[stri s.Environment = p.Environment } - return s, nil + return s, warnings, nil } // ExpandSteps injects the template for each // templated step in a yaml configuration. // //nolint:funlen,gocyclo // ignore function length -func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData, depth int) (*yaml.Build, error) { +func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData, warnings []string, depth int) (*yaml.Build, []string, error) { if len(tmpls) == 0 { - return s, nil + return s, warnings, nil } // return if max template depth has been reached if depth == 0 { retErr := fmt.Errorf("max template depth of %d exceeded", c.GetTemplateDepth()) - return s, retErr + return s, warnings, retErr } steps := yaml.StepSlice{} @@ -81,7 +86,7 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin // lookup step template name tmpl, ok := tmpls[step.Template.Name] if !ok { - return s, fmt.Errorf("missing template source for template %s in pipeline for step %s", step.Template.Name, step.Name) + return s, warnings, fmt.Errorf("missing template source for template %s in pipeline for step %s", step.Template.Name, step.Name) } // if ruledata is nil (CompileLite), continue with expansion @@ -94,7 +99,7 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin pipeline, err := pipeline.Purge(r) if err != nil { - return nil, fmt.Errorf("unable to purge pipeline: %w", err) + return nil, warnings, fmt.Errorf("unable to purge pipeline: %w", err) } // if step purged, do not proceed with expansion @@ -115,7 +120,7 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin // inject environment information for template step, err := c.EnvironmentStep(step, envGlobalSteps) if err != nil { - return s, err + return s, warnings, err } var ( @@ -126,7 +131,7 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin if bytes, found = c.TemplateCache[tmpl.Source]; !found { bytes, err = c.getTemplate(ctx, tmpl, step.Template.Name) if err != nil { - return s, err + return s, warnings, err } } @@ -138,23 +143,25 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin // inject template name into variables step.Template.Variables["VELA_TEMPLATE_NAME"] = step.Template.Name - tmplBuild, _, err := c.mergeTemplate(bytes, tmpl, step) + tmplBuild, tmplWarnings, err := c.mergeTemplate(bytes, tmpl, step) if err != nil { - return s, err + return s, warnings, err } + warnings = append(warnings, tmplWarnings...) + // if template references other templates, expand again if len(tmplBuild.Templates) != 0 { // if the tmplBuild has render_inline but the parent build does not, abort if tmplBuild.Metadata.RenderInline && !s.Metadata.RenderInline { - return s, fmt.Errorf("cannot use render_inline inside a called template (%s)", step.Template.Name) + return s, warnings, fmt.Errorf("cannot use render_inline inside a called template (%s)", step.Template.Name) } templates = append(templates, tmplBuild.Templates...) - tmplBuild, err = c.ExpandSteps(ctx, tmplBuild, mapFromTemplates(tmplBuild.Templates), r, depth-1) + tmplBuild, warnings, err = c.ExpandSteps(ctx, tmplBuild, mapFromTemplates(tmplBuild.Templates), r, warnings, depth-1) if err != nil { - return s, err + return s, warnings, err } } @@ -217,7 +224,7 @@ func (c *client) ExpandSteps(ctx context.Context, s *yaml.Build, tmpls map[strin s.Environment = environment s.Templates = templates - return s, nil + return s, warnings, nil } // ExpandDeployment injects the template for a diff --git a/compiler/native/expand_test.go b/compiler/native/expand_test.go index a47df02ed..ca85ef618 100644 --- a/compiler/native/expand_test.go +++ b/compiler/native/expand_test.go @@ -152,7 +152,7 @@ func TestNative_ExpandStages(t *testing.T) { } compiler.PrivateGithub = nil - _, err = compiler.ExpandStages( + _, _, err = compiler.ExpandStages( context.Background(), &yaml.Build{ Stages: stages, @@ -161,6 +161,7 @@ func TestNative_ExpandStages(t *testing.T) { }, tmpls, new(pipeline.RuleData), + nil, ) if err == nil { @@ -173,7 +174,7 @@ func TestNative_ExpandStages(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - build, err := compiler.ExpandStages( + build, _, err := compiler.ExpandStages( context.Background(), &yaml.Build{ Stages: stages, @@ -182,6 +183,7 @@ func TestNative_ExpandStages(t *testing.T) { }, tmpls, new(pipeline.RuleData), + nil, ) if err != nil { @@ -360,14 +362,14 @@ func TestNative_ExpandSteps(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - build, err := compiler.ExpandSteps( + build, _, err := compiler.ExpandSteps( context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment, }, - test.tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + test.tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err != nil { t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) } @@ -391,6 +393,106 @@ func TestNative_ExpandSteps(t *testing.T) { } } +func TestNative_ExpandStepsWarnings(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + set.String("clone-image", defaultCloneImage, "doc") + c := cli.NewContext(nil, set, nil) + + testRepo := new(api.Repo) + + testRepo.SetID(1) + testRepo.SetOrg("foo") + testRepo.SetName("bar") + + tests := []struct { + name string + tmpls map[string]*yaml.Template + }{ + { + name: "warnings", + tmpls: map[string]*yaml.Template{ + "warnings": { + Name: "steps_merge_anchor_1.yml", + Source: "github.example.com/foo/bar/steps_merge_anchor_1.yml", + Type: "github", + }, + }, + }, + } + + steps := yaml.StepSlice{ + &yaml.Step{ + Name: "sample", + Template: yaml.StepTemplate{ + Name: "warnings", + }, + }, + } + + globalEnvironment := raw.StringSliceMap{ + "foo": "test1", + "bar": "test2", + } + + wantWarnings := []string{ + "[warnings]:25:duplicate << keys in single YAML map", + "[warnings]:32:duplicate << keys in single YAML map", + "[warnings]:44:duplicate << keys in single YAML map", + "[warnings]:43:duplicate << keys in single YAML map", + } + + // run test + compiler, err := FromCLIContext(c) + if err != nil { + t.Errorf("Creating new compiler returned err: %v", err) + } + + compiler.WithCommit("123abc456def").WithRepo(testRepo) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, warnings, err := compiler.ExpandSteps( + context.Background(), + &yaml.Build{ + Steps: steps, + Services: yaml.ServiceSlice{}, + Environment: globalEnvironment, + }, + test.tmpls, new(pipeline.RuleData), []string{}, compiler.GetTemplateDepth()) + if err != nil { + t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(warnings, wantWarnings) { + t.Errorf("ExpandSteps()_Type%s returned incorrect warnings: %v", test.name, warnings) + } + }) + } +} + func TestNative_ExpandDeployment(t *testing.T) { // setup context gin.SetMode(gin.TestMode) @@ -741,13 +843,13 @@ func TestNative_ExpandStepsMulti(t *testing.T) { ruledata := new(pipeline.RuleData) ruledata.Branch = "main" - build, err := compiler.ExpandSteps(context.Background(), + build, _, err := compiler.ExpandSteps(context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}, }, - tmpls, ruledata, compiler.GetTemplateDepth()) + tmpls, ruledata, nil, compiler.GetTemplateDepth()) if err != nil { t.Errorf("ExpandSteps returned err: %v", err) } @@ -838,14 +940,14 @@ func TestNative_ExpandStepsStarlark(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - build, err := compiler.ExpandSteps(context.Background(), + build, _, err := compiler.ExpandSteps(context.Background(), &yaml.Build{ Steps: steps, Secrets: yaml.SecretSlice{}, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}, }, - tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err != nil { t.Errorf("ExpandSteps returned err: %v", err) } @@ -1025,13 +1127,13 @@ func TestNative_ExpandSteps_TemplateCallTemplate(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - build, err := compiler.ExpandSteps(context.Background(), + build, _, err := compiler.ExpandSteps(context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment, Templates: yaml.TemplateSlice{test.tmpls["chain"]}, }, - test.tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + test.tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err != nil { t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) } @@ -1259,14 +1361,14 @@ func TestNative_ExpandStepsDuplicateCalls(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - build, err := compiler.ExpandSteps( + build, _, err := compiler.ExpandSteps( context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment, }, - test.tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + test.tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err != nil { t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) } @@ -1369,11 +1471,11 @@ func TestNative_ExpandSteps_TemplateCallTemplate_CircularFail(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := compiler.ExpandSteps(context.Background(), + _, _, err := compiler.ExpandSteps(context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment, }, - test.tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + test.tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err == nil { t.Errorf("ExpandSteps_Type%s should have returned an error", test.name) } @@ -1460,13 +1562,13 @@ func TestNative_ExpandSteps_CallTemplateWithRenderInline(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := compiler.ExpandSteps(context.Background(), + _, _, err := compiler.ExpandSteps(context.Background(), &yaml.Build{ Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment, }, - test.tmpls, new(pipeline.RuleData), compiler.GetTemplateDepth()) + test.tmpls, new(pipeline.RuleData), nil, compiler.GetTemplateDepth()) if err == nil { t.Errorf("ExpandSteps_Type%s should have returned an error", test.name) } diff --git a/compiler/native/parse.go b/compiler/native/parse.go index b9f47e4dc..57b0a176c 100644 --- a/compiler/native/parse.go +++ b/compiler/native/parse.go @@ -112,7 +112,7 @@ func (c *client) Parse(v interface{}, pipelineType string, template *yaml.Templa // ParseBytes converts a byte slice to a yaml configuration. func ParseBytes(data []byte) (*yaml.Build, []byte, []string, error) { - config, warnings, err := internal.ParseYAML(data) + config, warnings, err := internal.ParseYAML(data, "") if err != nil { return nil, nil, nil, err } diff --git a/compiler/template/native/render.go b/compiler/template/native/render.go index 41ff0560a..bbf32e78f 100644 --- a/compiler/template/native/render.go +++ b/compiler/template/native/render.go @@ -45,7 +45,7 @@ func Render(tmpl string, name string, tName string, environment raw.StringSliceM } // unmarshal the template to the pipeline - config, warnings, err := internal.ParseYAML(buffer.Bytes()) + config, warnings, err := internal.ParseYAML(buffer.Bytes(), tName) if err != nil { return nil, nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } @@ -99,7 +99,7 @@ func RenderBuild(tmpl string, b string, envs map[string]string, variables map[st } // unmarshal the template to the pipeline - config, warnings, err := internal.ParseYAML(buffer.Bytes()) + config, warnings, err := internal.ParseYAML(buffer.Bytes(), "") if err != nil { return nil, nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } diff --git a/compiler/template/starlark/render.go b/compiler/template/starlark/render.go index d9c1e4bd0..d191ac1fc 100644 --- a/compiler/template/starlark/render.go +++ b/compiler/template/starlark/render.go @@ -121,7 +121,7 @@ func Render(tmpl string, name string, tName string, environment raw.StringSliceM } // unmarshal the template to the pipeline - config, warnings, err := internal.ParseYAML(buf.Bytes()) + config, warnings, err := internal.ParseYAML(buf.Bytes(), tName) if err != nil { return nil, nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } @@ -235,7 +235,7 @@ func RenderBuild(tmpl string, b string, envs map[string]string, variables map[st } // unmarshal the template to the pipeline - config, warnings, err := internal.ParseYAML(buf.Bytes()) + config, warnings, err := internal.ParseYAML(buf.Bytes(), "") if err != nil { return nil, nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } diff --git a/compiler/types/raw/map.go b/compiler/types/raw/map.go index ed569949a..deb1c81d0 100644 --- a/compiler/types/raw/map.go +++ b/compiler/types/raw/map.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "encoding/json" "errors" + "fmt" "strings" "github.com/invopop/jsonschema" @@ -138,7 +139,7 @@ func (s *StringSliceMap) UnmarshalYAML(unmarshal func(interface{}) error) error return nil } - return errors.New("unable to unmarshal into StringSliceMap") + return fmt.Errorf("unable to unmarshal into StringSliceMap: %w", err) } // JSONSchema handles some overrides that need to be in place diff --git a/internal/yaml.go b/internal/yaml.go index 22280df7b..ef621df6a 100644 --- a/internal/yaml.go +++ b/internal/yaml.go @@ -14,13 +14,18 @@ import ( ) // ParseYAML is a helper function for transitioning teams away from legacy buildkite YAML parser. -func ParseYAML(data []byte) (*types.Build, []string, error) { +func ParseYAML(data []byte, tmplName string) (*types.Build, []string, error) { var ( - rootNode yaml.Node - warnings []string - version string + rootNode yaml.Node + warnings []string + version string + warningPrefix string ) + if len(tmplName) > 0 { + warningPrefix = fmt.Sprintf("[%s]:", tmplName) + } + err := yaml.Unmarshal(data, &rootNode) if err != nil { return nil, nil, fmt.Errorf("unable to unmarshal pipeline version yaml: %w", err) @@ -53,7 +58,7 @@ func ParseYAML(data []byte) (*types.Build, []string, error) { config = legacyConfig.ToYAML() - warnings = append(warnings, `using legacy version - address any incompatibilities and use "1" instead`) + warnings = append(warnings, fmt.Sprintf(`%susing legacy version - address any incompatibilities and use "1" instead`, warningPrefix)) default: // unmarshal the bytes into the yaml configuration @@ -69,7 +74,7 @@ func ParseYAML(data []byte) (*types.Build, []string, error) { return nil, nil, err } - warnings = collapseMergeAnchors(root.Content[0], warnings) + warnings = collapseMergeAnchors(root.Content[0], warnings, warningPrefix) newData, err := yaml.Marshal(root) if err != nil { @@ -90,7 +95,7 @@ func ParseYAML(data []byte) (*types.Build, []string, error) { } // collapseMergeAnchors traverses the entire pipeline and replaces duplicate `<<` keys with a single key->sequence. -func collapseMergeAnchors(node *yaml.Node, warnings []string) []string { +func collapseMergeAnchors(node *yaml.Node, warnings []string, warningPrefix string) []string { // only replace on maps if node.Kind == yaml.MappingNode { var ( @@ -132,18 +137,18 @@ func collapseMergeAnchors(node *yaml.Node, warnings []string) []string { for i := len(keysToRemove) - 1; i >= 0; i-- { index := keysToRemove[i] - warnings = append(warnings, fmt.Sprintf("%d:duplicate << keys in single YAML map", node.Content[index].Line)) + warnings = append(warnings, fmt.Sprintf("%s%d:duplicate << keys in single YAML map", warningPrefix, node.Content[index].Line)) node.Content = append(node.Content[:index], node.Content[index+2:]...) } } // go to next level for _, content := range node.Content { - warnings = collapseMergeAnchors(content, warnings) + warnings = collapseMergeAnchors(content, warnings, warningPrefix) } } else if node.Kind == yaml.SequenceNode { for _, item := range node.Content { - warnings = collapseMergeAnchors(item, warnings) + warnings = collapseMergeAnchors(item, warnings, warningPrefix) } } diff --git a/internal/yaml_test.go b/internal/yaml_test.go index f7126666a..3ee72f9c0 100644 --- a/internal/yaml_test.go +++ b/internal/yaml_test.go @@ -36,11 +36,12 @@ func TestInternal_ParseYAML(t *testing.T) { // set up tests tests := []struct { - name string - file string - wantBuild *yaml.Build - wantWarnings []string - wantErr bool + name string + file string + wantBuild *yaml.Build + wantWarnings []string + warningPrefix string + wantErr bool }{ { name: "go-yaml", @@ -71,6 +72,13 @@ func TestInternal_ParseYAML(t *testing.T) { wantBuild: wantBuild, wantWarnings: []string{"16:duplicate << keys in single YAML map"}, }, + { + name: "anchor collapse - warning prefix", + file: "testdata/buildkite_new_version.yml", + wantBuild: wantBuild, + wantWarnings: []string{"[prefix]:16:duplicate << keys in single YAML map"}, + warningPrefix: "prefix", + }, { name: "no version", file: "testdata/no_version.yml", @@ -91,7 +99,7 @@ func TestInternal_ParseYAML(t *testing.T) { t.Errorf("unable to read file for test %s: %v", test.name, err) } - gotBuild, gotWarnings, err := ParseYAML(bytes) + gotBuild, gotWarnings, err := ParseYAML(bytes, test.warningPrefix) if err != nil && !test.wantErr { t.Errorf("ParseYAML for test %s returned err: %v", test.name, err) }