From 26b998456a566989e054f18f5dc8934fa3fc904d Mon Sep 17 00:00:00 2001 From: Dan Duvall Date: Wed, 4 Apr 2018 15:28:21 -0700 Subject: Introduce strict/versioned config parsing Summary: Introduced a `version` config field that must be specified and match `config.CurrentVersion`. Changed `config.ReadConfig` to use `yaml.UnmarshalStrict` to ensure that errors are surfaced when unknown/bad fields are present in the given YAML config. A smaller `config.VersionConfig` is now unmarshaled first to prevalidate the new `version` field before the entire config is parsed. Fixes T191460 Test Plan: Run `go test ./...`. Run `blubber` against some configuration containing invalid fields and ensure that it surfaces a YAML error. Reviewers: thcipriani, demon, hashar, mmodell, mobrovac, #release-engineering-team Reviewed By: thcipriani, #release-engineering-team Tags: #release-engineering-team Maniphest Tasks: T191460 Differential Revision: https://phabricator.wikimedia.org/D1021 --- blubber.example.yaml | 1 + config/apt_test.go | 3 +++ config/artifacts_test.go | 4 ++++ config/common_test.go | 4 ++++ config/config.go | 5 +++-- config/config_test.go | 15 +++++++++++++++ config/flag_test.go | 1 + config/lives_test.go | 12 ++++++++++++ config/node_test.go | 4 ++++ config/python_test.go | 3 +++ config/reader.go | 20 ++++++++++++++++++-- config/reader_test.go | 32 ++++++++++++++++++++++++++++++++ config/runs_test.go | 12 ++++++++++++ config/validation.go | 26 ++++++++++++++------------ config/variant_test.go | 7 +++++++ config/version.go | 12 ++++++++++++ config/version_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ docker/compiler_test.go | 4 ++++ 18 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 config/version.go create mode 100644 config/version_test.go diff --git a/blubber.example.yaml b/blubber.example.yaml index e47eefb..e4fe3c9 100644 --- a/blubber.example.yaml +++ b/blubber.example.yaml @@ -1,4 +1,5 @@ --- +version: v1 base: debian:jessie apt: packages: [libjpeg, libyaml] diff --git a/config/apt_test.go b/config/apt_test.go index 2f0df11..480c1b7 100644 --- a/config/apt_test.go +++ b/config/apt_test.go @@ -12,6 +12,7 @@ import ( func TestAptConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 apt: packages: - libfoo @@ -66,6 +67,7 @@ func TestAptConfigValidation(t *testing.T) { t.Run("packages", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 apt: packages: - f1 @@ -81,6 +83,7 @@ func TestAptConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 apt: packages: - foo diff --git a/config/artifacts_test.go b/config/artifacts_test.go index 85bad02..3092572 100644 --- a/config/artifacts_test.go +++ b/config/artifacts_test.go @@ -11,6 +11,7 @@ import ( func TestArtifactsConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo variants: build: {} @@ -79,6 +80,7 @@ func TestArtifactsConfigValidation(t *testing.T) { t.Run("from", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: @@ -92,6 +94,7 @@ func TestArtifactsConfigValidation(t *testing.T) { t.Run("missing", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: @@ -109,6 +112,7 @@ func TestArtifactsConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: diff --git a/config/common_test.go b/config/common_test.go index 20a07f5..537c025 100644 --- a/config/common_test.go +++ b/config/common_test.go @@ -10,6 +10,7 @@ import ( func TestCommonConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: fooimage sharedvolume: true entrypoint: ["/bin/foo"] @@ -33,6 +34,7 @@ func TestCommonConfigValidation(t *testing.T) { t.Run("base", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 base: foo variants: {}`)) @@ -41,6 +43,7 @@ func TestCommonConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 base: variants: {}`)) @@ -49,6 +52,7 @@ func TestCommonConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 base: foo fighter variants: {}`)) diff --git a/config/config.go b/config/config.go index 46be3d1..695759f 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ package config // Config holds the root fields of a Blubber configuration. // type Config struct { - CommonConfig `yaml:",inline"` - Variants map[string]VariantConfig `yaml:"variants" validate:"variants,dive"` + CommonConfig `yaml:",inline"` + Variants map[string]VariantConfig `yaml:"variants" validate:"variants,dive"` + VersionConfig `yaml:",inline"` } diff --git a/config/config_test.go b/config/config_test.go index 051e110..f6b6c1e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -8,10 +8,24 @@ import ( "phabricator.wikimedia.org/source/blubber/config" ) +func TestConfig(t *testing.T) { + cfg, err := config.ReadConfig([]byte(`--- + version: v1 + variants: + foo: {}`)) + + if assert.NoError(t, err) { + assert.Equal(t, "v1", cfg.Version) + assert.Contains(t, cfg.Variants, "foo") + assert.IsType(t, config.VariantConfig{}, cfg.Variants["foo"]) + } +} + func TestConfigValidation(t *testing.T) { t.Run("variants", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: {}`)) @@ -21,6 +35,7 @@ func TestConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build foo: {} foo bar: {}`)) diff --git a/config/flag_test.go b/config/flag_test.go index d2651f6..e7481a6 100644 --- a/config/flag_test.go +++ b/config/flag_test.go @@ -10,6 +10,7 @@ import ( func TestFlagOverwrite(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo node: { dependencies: true } sharedvolume: false diff --git a/config/lives_test.go b/config/lives_test.go index dec1f99..d6097b5 100644 --- a/config/lives_test.go +++ b/config/lives_test.go @@ -11,6 +11,7 @@ import ( func TestLivesConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo lives: in: /some/directory @@ -39,6 +40,7 @@ func TestLivesConfig(t *testing.T) { func TestLivesConfigDefaults(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo`)) if assert.NoError(t, err) { @@ -94,6 +96,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("in", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: in: /foo`)) @@ -102,6 +105,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: {}`)) assert.False(t, config.IsValidationError(err)) @@ -109,6 +113,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("non-root", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: in: /`)) @@ -121,6 +126,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("non-root tricky", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: in: /foo/..`)) @@ -133,6 +139,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("absolute", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: in: foo/bar`)) @@ -147,6 +154,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("as", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: as: foo-bar.baz`)) @@ -155,6 +163,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: {}`)) assert.False(t, config.IsValidationError(err)) @@ -162,6 +171,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("no spaces", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: as: foo bar`)) @@ -174,6 +184,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("long enough", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: as: fo`)) @@ -186,6 +197,7 @@ func TestLivesConfigValidation(t *testing.T) { t.Run("not root", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 lives: as: root`)) diff --git a/config/node_test.go b/config/node_test.go index 5e0c4f3..6b7519a 100644 --- a/config/node_test.go +++ b/config/node_test.go @@ -11,6 +11,7 @@ import ( func TestNodeConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo node: dependencies: true @@ -162,6 +163,7 @@ func TestNodeConfigValidation(t *testing.T) { t.Run("env", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 node: env: production`)) @@ -170,6 +172,7 @@ func TestNodeConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 node: {}`)) assert.False(t, config.IsValidationError(err)) @@ -177,6 +180,7 @@ func TestNodeConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 node: env: foo bar`)) diff --git a/config/python_test.go b/config/python_test.go index 5912ae2..55e49c7 100644 --- a/config/python_test.go +++ b/config/python_test.go @@ -11,6 +11,7 @@ import ( func TestPythonConfigUnmarshalMerge(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo python: version: python2.7 @@ -36,6 +37,7 @@ func TestPythonConfigUnmarshalMerge(t *testing.T) { func TestPythonConfigMergeEmpty(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo python: requirements: [requirements.txt] @@ -57,6 +59,7 @@ func TestPythonConfigMergeEmpty(t *testing.T) { func TestPythonConfigDoNotMergeNil(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo python: requirements: [requirements.txt] diff --git a/config/reader.go b/config/reader.go index 378d523..4386aa7 100644 --- a/config/reader.go +++ b/config/reader.go @@ -91,11 +91,27 @@ func ExpandVariant(config *Config, name string) (*VariantConfig, error) { // ReadConfig unmarshals the given YAML bytes into a new Config struct. // func ReadConfig(data []byte) (*Config, error) { - var config Config + var ( + version VersionConfig + config Config + ) + // Unmarshal (un-strictly) config version first for pre-validation + err := yaml.Unmarshal(data, &version) + + if err != nil { + return nil, err + } + + if err = Validate(version); err != nil { + return nil, err + } + + // Unmarshal the default config yaml.Unmarshal([]byte(DefaultConfig), &config) - err := yaml.Unmarshal(data, &config) + // And finally strictly unmarshal the entire user-provided config + err = yaml.UnmarshalStrict(data, &config) if err != nil { return nil, err diff --git a/config/reader_test.go b/config/reader_test.go index e11e7af..51269ea 100644 --- a/config/reader_test.go +++ b/config/reader_test.go @@ -11,6 +11,7 @@ import ( func ExampleResolveIncludes() { cfg, _ := config.ReadConfig([]byte(`--- + version: v1 variants: varA: { includes: [varB, varC] } varB: { includes: [varD, varE] } @@ -26,8 +27,37 @@ func ExampleResolveIncludes() { // Output: [varF varD varE varB varC varA] } +func TestReadConfig_ErrorsOnUnknownYAML(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + version: v1 + newphone: whodis + variants: + foo: {}`)) + + assert.EqualError(t, + err, + "yaml: unmarshal errors:\n"+ + " line 2: field newphone not found in struct config.Config", + ) +} + +func TestReadConfig_ValidateVersionBeforeStrictUnmarshal(t *testing.T) { + _, err := config.ReadConfig([]byte(`--- + version: foo + newphone: whodis + variants: + foo: {}`)) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `version: config version "foo" is unsupported`, msg) + } +} + func TestResolveIncludesPreventsInfiniteRecursion(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 variants: varA: { includes: [varB] } varB: { includes: [varA] }`)) @@ -41,6 +71,7 @@ func TestResolveIncludesPreventsInfiniteRecursion(t *testing.T) { func TestMultiLevelIncludes(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: nodejs-slim variants: build: @@ -70,6 +101,7 @@ func TestMultiLevelIncludes(t *testing.T) { func TestMultiIncludes(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 variants: mammal: base: neutral diff --git a/config/runs_test.go b/config/runs_test.go index 18d3726..8ae87fc 100644 --- a/config/runs_test.go +++ b/config/runs_test.go @@ -11,6 +11,7 @@ import ( func TestRunsConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo runs: as: someuser @@ -85,6 +86,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("as", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: as: foo-bar.baz`)) @@ -93,6 +95,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: {}`)) assert.False(t, config.IsValidationError(err)) @@ -100,6 +103,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("no spaces", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: as: foo bar`)) @@ -112,6 +116,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("long enough", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: as: fo`)) @@ -124,6 +129,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("not root", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: as: root`)) @@ -138,6 +144,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("environment", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: environment: foo: bar @@ -152,6 +159,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: {}`)) assert.False(t, config.IsValidationError(err)) @@ -160,6 +168,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { t.Run("spaces", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: environment: foo fighter: bar`)) @@ -173,6 +182,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("dashes", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: environment: foo-fighter: bar`)) @@ -186,6 +196,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("dots", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: environment: foo.fighter: bar`)) @@ -199,6 +210,7 @@ func TestRunsConfigValidation(t *testing.T) { t.Run("starts with number", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 runs: environment: 1foo: bar`)) diff --git a/config/validation.go b/config/validation.go index 646e49d..2529e00 100644 --- a/config/validation.go +++ b/config/validation.go @@ -31,20 +31,22 @@ var ( variantNameRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-\.]+[a-zA-Z0-9]$`) humanizedErrors = map[string]string{ - "abspath": `{{.Field}}: "{{.Value}}" is not a valid absolute non-root path`, - "baseimage": `{{.Field}}: "{{.Value}}" is not a valid base image reference`, - "debianpackage": `{{.Field}}: "{{.Value}}" is not a valid Debian package name`, - "envvars": `{{.Field}}: contains invalid environment variable names`, - "nodeenv": `{{.Field}}: "{{.Value}}" is not a valid Node environment name`, - "required": `{{.Field}}: is required`, - "username": `{{.Field}}: "{{.Value}}" is not a valid user name`, - "variantref": `{{.Field}}: references an unknown variant "{{.Value}}"`, - "variants": `{{.Field}}: contains a bad variant name`, + "abspath": `{{.Field}}: "{{.Value}}" is not a valid absolute non-root path`, + "baseimage": `{{.Field}}: "{{.Value}}" is not a valid base image reference`, + "currentversion": `{{.Field}}: config version "{{.Value}}" is unsupported`, + "debianpackage": `{{.Field}}: "{{.Value}}" is not a valid Debian package name`, + "envvars": `{{.Field}}: contains invalid environment variable names`, + "nodeenv": `{{.Field}}: "{{.Value}}" is not a valid Node environment name`, + "required": `{{.Field}}: is required`, + "username": `{{.Field}}: "{{.Value}}" is not a valid user name`, + "variantref": `{{.Field}}: references an unknown variant "{{.Value}}"`, + "variants": `{{.Field}}: contains a bad variant name`, } validatorAliases = map[string]string{ - "nodeenv": "alphanum", - "username": "hostname,ne=root", + "currentversion": "eq=" + CurrentVersion, + "nodeenv": "alphanum", + "username": "hostname,ne=root", } validatorFuncs = map[string]validator.FuncCtx{ @@ -86,7 +88,7 @@ func newValidator() *validator.Validate { // Config value. If the returned error is not nil, it will contain a // user-friendly message describing all invalid field values. // -func Validate(config Config) error { +func Validate(config interface{}) error { validate := newValidator() ctx := context.WithValue(context.Background(), rootCfgCtx, config) diff --git a/config/variant_test.go b/config/variant_test.go index a2f52c9..f5216f6 100644 --- a/config/variant_test.go +++ b/config/variant_test.go @@ -12,6 +12,7 @@ import ( func TestVariantConfig(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo variants: build: {} @@ -215,6 +216,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("includes", func(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: { includes: [build] }`)) @@ -224,6 +226,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: {}`)) @@ -233,6 +236,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: { includes: [build, foobuild, foo_build] }`)) @@ -252,6 +256,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("ok", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: { copies: build }`)) @@ -261,6 +266,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("optional", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: {}`)) @@ -270,6 +276,7 @@ func TestVariantConfigValidation(t *testing.T) { t.Run("bad", func(t *testing.T) { _, err := config.ReadConfig([]byte(`--- + version: v1 variants: build: {} foo: { copies: foobuild }`)) diff --git a/config/version.go b/config/version.go new file mode 100644 index 0000000..4a145fd --- /dev/null +++ b/config/version.go @@ -0,0 +1,12 @@ +package config + +// CurrentVersion declares the currently supported config version. +// +const CurrentVersion string = "v1" + +// VersionConfig contains a single field that allows for validation of the +// config version independent from an entire Config struct. +// +type VersionConfig struct { + Version string `yaml:"version" validate:"required,currentversion"` +} diff --git a/config/version_test.go b/config/version_test.go new file mode 100644 index 0000000..07df93e --- /dev/null +++ b/config/version_test.go @@ -0,0 +1,44 @@ +package config_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "phabricator.wikimedia.org/source/blubber/config" +) + +func TestVersionConfig_YAML(t *testing.T) { + cfg, err := config.ReadConfig([]byte(`--- + version: v1 + variants: + foo: {}`)) + + assert.Nil(t, err) + + if assert.NoError(t, err) { + assert.Equal(t, "v1", cfg.Version) + } +} + +func TestVersionConfig_Validation(t *testing.T) { + t.Run("supported version", func(t *testing.T) { + err := config.Validate(config.VersionConfig{ + Version: "v1", + }) + + assert.False(t, config.IsValidationError(err)) + }) + + t.Run("unsupported version", func(t *testing.T) { + err := config.Validate(config.VersionConfig{ + Version: "v2", + }) + + if assert.True(t, config.IsValidationError(err)) { + msg := config.HumanizeValidationError(err) + + assert.Equal(t, `version: config version "v2" is unsupported`, msg) + } + }) +} diff --git a/docker/compiler_test.go b/docker/compiler_test.go index 846304b..c5e2a28 100644 --- a/docker/compiler_test.go +++ b/docker/compiler_test.go @@ -13,6 +13,7 @@ import ( func TestSingleStageHasNoName(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo/bar variants: development: {}`)) @@ -27,6 +28,7 @@ func TestSingleStageHasNoName(t *testing.T) { func TestMultiStageIncludesStageNames(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo/bar variants: build: {} @@ -50,6 +52,7 @@ func TestMultiStageIncludesStageNames(t *testing.T) { func TestMultipleArtifactsFromSameStage(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo/bar variants: build: {} @@ -76,6 +79,7 @@ func TestMultipleArtifactsFromSameStage(t *testing.T) { func TestMetaDataLabels(t *testing.T) { cfg, err := config.ReadConfig([]byte(`--- + version: v1 base: foo/bar variants: development: {}`)) -- cgit v1.2.1