nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: compose-spec/compose-go
Pull request #345 (2023-01-13 11:24:36)

Allow brackets in env vars
Signed-off-by: Laura Brehm <laurabrehm@hey.com>

Allows using environment variables containing square brackets, such as `kinesis.property[1].propertyId=1`, to align with V1 behavior.

Fixes https://github.com/compose-spec/compose-go/issues/337


Patch:
diff --git a/README.md b/README.md
index 9c34a80..c2da22f 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@ Go reference library for parsing and loading Compose files as specified by the
 
 ## Used by
 
-* [compose](https://github.com/docker/compose)
+* [compose-ref](https://github.com/compose-spec/compose-ref)
 * [containerd/nerdctl](https://github.com/containerd/nerdctl)
 * [compose-cli](https://github.com/docker/compose-cli)
 * [tilt.dev](https://github.com/tilt-dev/tilt)
-* [kompose](https://github.com/kubernetes/kompose)
diff --git a/dotenv/parser.go b/dotenv/parser.go
index e046619..cc6f933 100644
--- a/dotenv/parser.go
+++ b/dotenv/parser.go
@@ -117,7 +117,7 @@ loop:
 			offset = i + 1
 			inherited = char == '\n'
 			break loop
-		case '_', '.', '-':
+		case '_', '.', '-', '[', ']':
 		default:
 			// variable name should match [A-Za-z0-9_.-]
 			if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) {


Test patch:
diff --git a/dotenv/parser_test.go b/dotenv/parser_test.go
new file mode 100644
index 0000000..5df05b3
--- /dev/null
+++ b/dotenv/parser_test.go
@@ -0,0 +1,33 @@
+package dotenv
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+var testInput = `
+a=b
+a[1]=c
+a.propertyKey=d
+`
+
+func TestParseBytes(t *testing.T) {
+	p := newParser()
+
+	var inputBytes = []byte(testInput)
+	expectedOutput := map[string]string{
+		"a":             "b",
+		"a[1]":          "c",
+		"a.propertyKey": "d",
+	}
+
+	out := map[string]string{}
+	err := p.parseBytes([]byte(inputBytes), out, nil)
+
+	assert.NilError(t, err)
+	assert.Equal(t, len(expectedOutput), len(out))
+	for key, value := range expectedOutput {
+		assert.Equal(t, value, out[key])
+	}
+}
Repository: compose-spec/compose-go
Pull request #347 (2023-01-17 09:11:49)

interpolate name set in yaml file
closes https://github.com/docker/compose/issues/10171


Patch:
diff --git a/loader/loader.go b/loader/loader.go
index 6afa79f..c8ac317 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -161,7 +161,10 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 		op(opts)
 	}
 
-	projectName := projectName(configDetails, opts)
+	projectName, err := projectName(configDetails, opts)
+	if err != nil {
+		return nil, err
+	}
 
 	var configs []*types.Config
 	for i, file := range configDetails.ConfigFiles {
@@ -246,19 +249,26 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 	return project, nil
 }
 
-func projectName(details types.ConfigDetails, opts *Options) string {
+func projectName(details types.ConfigDetails, opts *Options) (string, error) {
 	projectName, projectNameImperativelySet := opts.GetProjectName()
 	var pjNameFromConfigFile string
 
 	for _, configFile := range details.ConfigFiles {
 		yml, err := ParseYAML(configFile.Content)
 		if err != nil {
-			return ""
+			return "", nil
 		}
 		if val, ok := yml["name"]; ok && val != "" {
 			pjNameFromConfigFile = yml["name"].(string)
 		}
 	}
+	if !opts.SkipInterpolation {
+		interpolated, err := interp.Interpolate(map[string]interface{}{"name": pjNameFromConfigFile}, *opts.Interpolate)
+		if err != nil {
+			return "", err
+		}
+		pjNameFromConfigFile = interpolated["name"].(string)
+	}
 	pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
 	if !projectNameImperativelySet && pjNameFromConfigFile != "" {
 		projectName = pjNameFromConfigFile
@@ -267,7 +277,7 @@ func projectName(details types.ConfigDetails, opts *Options) string {
 	if _, ok := details.Environment[consts.ComposeProjectName]; !ok && projectName != "" {
 		details.Environment[consts.ComposeProjectName] = projectName
 	}
-	return projectName
+	return projectName, nil
 }
 
 func NormalizeProjectName(s string) string {


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
index 3836177..ffbb5e5 100644
--- a/cli/options_test.go
+++ b/cli/options_test.go
@@ -144,6 +144,15 @@ func TestProjectName(t *testing.T) {
 		assert.Equal(t, p.Name, "my_project_from_dot_env")
 	})
 
+	t.Run("by name in compose.yaml with variable", func(t *testing.T) {
+		opts, err := NewProjectOptions([]string{"testdata/simple/compose-name.yaml"}, WithEnv([]string{
+			"TEST=expected",
+		}))
+		assert.NilError(t, err)
+		p, err := ProjectFromOptions(opts)
+		assert.NilError(t, err)
+		assert.Equal(t, p.Name, "test-expected-test")
+	})
 }
 
 func TestProjectFromSetOfFiles(t *testing.T) {
diff --git a/cli/testdata/simple/compose-name.yaml b/cli/testdata/simple/compose-name.yaml
new file mode 100644
index 0000000..a433f73
--- /dev/null
+++ b/cli/testdata/simple/compose-name.yaml
@@ -0,0 +1,4 @@
+name: test-${TEST}-test
+services:
+  simple:
+    image: nginx
Repository: compose-spec/compose-go
Pull request #349 (2023-01-23 13:36:23)

don't override healthcheck test with unset test command
closes https://github.com/docker/compose/issues/10188


Patch:
diff --git a/loader/merge.go b/loader/merge.go
index 3c24008..c6b25f5 100644
--- a/loader/merge.go
+++ b/loader/merge.go
@@ -130,7 +130,7 @@ func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConf
 	if overrideService.Command != nil {
 		baseService.Command = overrideService.Command
 	}
-	if overrideService.HealthCheck != nil {
+	if overrideService.HealthCheck != nil && overrideService.HealthCheck.Test != nil {
 		baseService.HealthCheck.Test = overrideService.HealthCheck.Test
 	}
 	if overrideService.Entrypoint != nil {


Test patch:
diff --git a/loader/merge_test.go b/loader/merge_test.go
index d080280..9b91922 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -1287,11 +1287,23 @@ func TestMergeHealthCheck(t *testing.T) {
 					},
 				},
 			}},
+			{Filename: "override.yml", Config: map[string]interface{}{
+				"services": map[string]interface{}{
+					"foo": map[string]interface{}{
+						"image": "alpine",
+						"healthcheck": map[string]interface{}{
+							"timeout": "30s",
+						},
+					},
+				},
+			}},
 		},
 	}
 	merged, err := loadTestProject(configDetails)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, merged.Services[0].HealthCheck.Test, types.HealthCheckTest{"CMD", "override"})
+	check := merged.Services[0].HealthCheck
+	assert.DeepEqual(t, check.Test, types.HealthCheckTest{"CMD", "override"})
+	assert.Equal(t, check.Timeout.String(), "30s")
 }
 
 func TestMergeEnvironments(t *testing.T) {
Repository: compose-spec/compose-go
Pull request #373 (2023-03-24 17:32:56)

Reject Compose files containing service with `network_mode` and `networks`
see: https://github.com/compose-spec/compose-spec/pull/323


Patch:
diff --git a/loader/validate.go b/loader/validate.go
index 455e48c..b4c42c7 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -51,6 +51,9 @@ func checkConsistency(project *types.Project) error {
 			}
 		}
 
+		if s.NetworkMode != "" && len(s.Networks) > 0 {
+			return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %s declares mutually exclusive `network_mode` and `networks`", s.Name))
+		}
 		for network := range s.Networks {
 			if _, ok := project.Networks[network]; !ok {
 				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined network %s", s.Name, network))


Test patch:
diff --git a/loader/validate_test.go b/loader/validate_test.go
index 577d407..959f7a3 100644
--- a/loader/validate_test.go
+++ b/loader/validate_test.go
@@ -138,6 +138,24 @@ func TestValidateNetworkMode(t *testing.T) {
 		err := checkConsistency(project)
 		assert.NilError(t, err)
 	})
+
+	t.Run("network_mode & networks can't both be defined", func(t *testing.T) {
+		project := &types.Project{
+			Networks: types.Networks{"mynetwork": types.NetworkConfig{}},
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:        "myservice1",
+					Image:       "scratch",
+					NetworkMode: "host",
+					Networks: map[string]*types.ServiceNetworkConfig{
+						"mynetwork": {},
+					},
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.Error(t, err, "service myservice1 declares mutually exclusive `network_mode` and `networks`: invalid compose project")
+	})
 }
 
 func TestValidateSecret(t *testing.T) {
Repository: compose-spec/compose-go
Pull request #383 (2023-04-04 08:12:31)

ignore .env can't be read if not explicitly set by user
fixes https://github.com/docker/compose/issues/10430

added a test case so we don't re-introduce this regression in the future

I'm a bit sad we can't (in a portable way) just check err == EISDIR after `os.ReadFile` and need to preventively check `os.Stat`. 


Patch:
diff --git a/cli/options.go b/cli/options.go
index ecae4d2..c1d54ee 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -277,12 +277,27 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames [
 		}
 		dotEnvFile = abs
 
-		b, err := os.ReadFile(dotEnvFile)
+		s, err := os.Stat(dotEnvFile)
 		if os.IsNotExist(err) {
-			if len(filenames) > 0 {
-				return nil, errors.Errorf("Couldn't read env file: %s", dotEnvFile)
+			if len(filenames) == 0 {
+				return envMap, nil
 			}
-			return envMap, nil
+			return envMap, errors.Errorf("Couldn't find env file: %s", dotEnvFile)
+		}
+		if err != nil {
+			return envMap, err
+		}
+
+		if s.IsDir() {
+			if len(filenames) == 0 {
+				return envMap, nil
+			}
+			return envMap, errors.Errorf("%s is a directory", dotEnvFile)
+		}
+
+		b, err := os.ReadFile(dotEnvFile)
+		if os.IsNotExist(err) {
+			return nil, errors.Errorf("Couldn't read env file: %s", dotEnvFile)
 		}
 		if err != nil {
 			return envMap, err


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
index 1b32295..8f9a081 100644
--- a/cli/options_test.go
+++ b/cli/options_test.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"gotest.tools/v3/assert"
@@ -308,3 +309,16 @@ func TestEnvMap(t *testing.T) {
 	m = utils.GetAsEqualsMap(l)
 	assert.Equal(t, m["foo"], "bar")
 }
+
+func TestGetEnvFromFile(t *testing.T) {
+	wd := t.TempDir()
+	f := filepath.Join(wd, ".env")
+	err := os.Mkdir(f, 0o700)
+	assert.NilError(t, err)
+
+	_, err = GetEnvFromFile(nil, wd, nil)
+	assert.NilError(t, err)
+
+	_, err = GetEnvFromFile(nil, wd, []string{f})
+	assert.Check(t, strings.HasSuffix(err.Error(), ".env is a directory"))
+}
Repository: compose-spec/compose-go
Pull request #384 (2023-04-04 16:10:46)

fix regression considering volumes-from: container a dependency
fix https://github.com/docker/compose/issues/10417


Patch:
diff --git a/loader/normalize.go b/loader/normalize.go
index 3c9e1ad..d915604 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -117,7 +117,7 @@ func Normalize(project *types.Project, resolvePaths bool) error {
 		}
 
 		for _, vol := range s.VolumesFrom {
-			if !strings.HasPrefix(s.Pid, types.ContainerPrefix) {
+			if !strings.HasPrefix(vol, types.ContainerPrefix) {
 				spec := strings.Split(vol, ":")
 				s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{
 					Condition: types.ServiceConditionStarted,


Test patch:
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index 1066a53..807e286 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -201,8 +201,11 @@ func TestNormalizeDependsOn(t *testing.T) {
 				NetworkMode: "service:zot",
 			},
 			{
-				Name:        "bar",
-				VolumesFrom: []string{"zot"},
+				Name: "bar",
+				VolumesFrom: []string{
+					"zot",
+					"container:xxx",
+				},
 			},
 			{
 				Name: "zot",
@@ -220,6 +223,7 @@ services:
       default: null
     volumes_from:
       - zot
+      - container:xxx
   foo:
     depends_on:
       bar:
Repository: compose-spec/compose-go
Pull request #386 (2023-04-11 18:59:10)

schema: fix name of `dockerfile_inline` field in YAML
Missing Go struct tag here, so the field name in YAML was actually `dockerfileinline`.

(This isn't a breaking change: it was a mistake/broken before and not even usable since the schema validation would prevent you from using the typo'd field name.)

See also:
* docker/compose#10456


Patch:
diff --git a/types/types.go b/types/types.go
index c59b540..226ddc8 100644
--- a/types/types.go
+++ b/types/types.go
@@ -296,7 +296,7 @@ func (s set) toSlice() []string {
 type BuildConfig struct {
 	Context            string                `yaml:",omitempty" json:"context,omitempty"`
 	Dockerfile         string                `yaml:",omitempty" json:"dockerfile,omitempty"`
-	DockerfileInline   string                `yaml:",omitempty" json:"dockerfile_inline,omitempty"`
+	DockerfileInline   string                `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"`
 	Args               MappingWithEquals     `yaml:",omitempty" json:"args,omitempty"`
 	SSH                SSHConfig             `yaml:"ssh,omitempty" json:"ssh,omitempty"`
 	Labels             Labels                `yaml:",omitempty" json:"labels,omitempty"`


Test patch:
diff --git a/types/types_test.go b/types/types_test.go
index 2ec5476..e4e7a13 100644
--- a/types/types_test.go
+++ b/types/types_test.go
@@ -17,7 +17,9 @@
 package types
 
 import (
+	"bytes"
 	"encoding/json"
+	"fmt"
 	"strings"
 	"testing"
 
@@ -329,3 +331,40 @@ func TestMarshalServiceEntrypoint(t *testing.T) {
 	}
 
 }
+
+func TestMarshalBuild_DockerfileInline(t *testing.T) {
+	b := BuildConfig{
+		DockerfileInline: "FROM alpine\n\n# echo the env\nRUN env\n\nENTRYPOINT /bin/echo\n",
+	}
+	out, err := yaml.Marshal(b)
+	assert.NilError(t, err)
+
+	const expected = `
+dockerfile_inline: |
+    FROM alpine
+
+    # echo the env
+    RUN env
+
+    ENTRYPOINT /bin/echo
+`
+	assert.Check(t, equalTrimSpace(out, expected))
+
+	// round-trip
+	var b2 BuildConfig
+	assert.NilError(t, yaml.Unmarshal(out, &b2))
+	assert.Check(t, equalTrimSpace(b.DockerfileInline, b2.DockerfileInline))
+}
+
+func equalTrimSpace(x interface{}, y interface{}) is.Comparison {
+	trim := func(v interface{}) interface{} {
+		switch vv := v.(type) {
+		case string:
+			return strings.TrimSpace(vv)
+		case []byte:
+			return string(bytes.TrimSpace(vv))
+		}
+		panic(fmt.Errorf("invalid type %T (value: %+v)", v, v))
+	}
+	return is.DeepEqual(trim(x), trim(y))
+}
Repository: compose-spec/compose-go
Pull request #388 (2023-04-13 21:22:26)

loader: fix panic on invalid type in `name` field
Adjust the cast logic to be safe so that there isn't a panic if a non-string value is used as `name`. The file will fail validation with a type error this way.


Patch:
diff --git a/loader/loader.go b/loader/loader.go
index 59f0ed9..b5c8639 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -271,6 +271,12 @@ func InvalidProjectNameErr(v string) error {
 	)
 }
 
+// projectName determines the canonical name to use for the project considering
+// the loader Options as well as `name` fields in Compose YAML fields (which
+// also support interpolation).
+//
+// TODO(milas): restructure loading so that we don't need to re-parse the YAML
+// here, as it's both wasteful and makes this code error-prone.
 func projectName(details types.ConfigDetails, opts *Options) (string, error) {
 	projectName, projectNameImperativelySet := opts.GetProjectName()
 
@@ -281,10 +287,22 @@ func projectName(details types.ConfigDetails, opts *Options) (string, error) {
 		for _, configFile := range details.ConfigFiles {
 			yml, err := ParseYAML(configFile.Content)
 			if err != nil {
+				// HACK: the way that loading is currently structured, this is
+				// a duplicative parse just for the `name`. if it fails, we
+				// give up but don't return the error, knowing that it'll get
+				// caught downstream for us
 				return "", nil
 			}
 			if val, ok := yml["name"]; ok && val != "" {
-				pjNameFromConfigFile = yml["name"].(string)
+				sVal, ok := val.(string)
+				if !ok {
+					// HACK: see above - this is a temporary parsed version
+					// that hasn't been schema-validated, but we don't want
+					// to be the ones to actually report that, so give up,
+					// knowing that it'll get caught downstream for us
+					return "", nil
+				}
+				pjNameFromConfigFile = sVal
 			}
 		}
 		if !opts.SkipInterpolation {


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 59902af..3476600 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2337,3 +2337,9 @@ func TestDeviceWriteBps(t *testing.T) {
 	})
 
 }
+
+func TestInvalidProjectNameType(t *testing.T) {
+	p, err := loadYAML(`name: 123`)
+	assert.Error(t, err, "name must be a string")
+	assert.Assert(t, is.Nil(p))
+}
Repository: compose-spec/compose-go
Pull request #397 (2023-05-10 00:16:29)

Update compose-spec.json
Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action


Patch:
diff --git a/loader/full-example.yml b/loader/full-example.yml
index b3b07f8..9f818f8 100644
--- a/loader/full-example.yml
+++ b/loader/full-example.yml
@@ -8,6 +8,8 @@ services:
         RUN echo "hello" > /world.txt
 
   foo:
+    annotations:
+      - com.example.foo=bar
     build:
       context: ./dir
       dockerfile: Dockerfile
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index c587125..53f3c1d 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -83,6 +83,7 @@
 
       "properties": {
         "deploy": {"$ref": "#/definitions/deployment"},
+        "annotations": {"$ref": "#/definitions/list_or_dict"},
         "build": {
           "oneOf": [
             {"type": "string"},
diff --git a/types/types.go b/types/types.go
index a30b018..203184b 100644
--- a/types/types.go
+++ b/types/types.go
@@ -88,6 +88,7 @@ type ServiceConfig struct {
 	Name     string   `yaml:"-" json:"-"`
 	Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"`
 
+	Annotations  Mapping      `yaml:"annotations,omitempty" json:"annotations,omitempty"`
 	Build        *BuildConfig `yaml:"build,omitempty" json:"build,omitempty"`
 	BlkioConfig  *BlkioConfig `yaml:"blkio_config,omitempty" json:"blkio_config,omitempty"`
 	CapAdd       []string     `yaml:"cap_add,omitempty" json:"cap_add,omitempty"`


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index ff68a12..82924dd 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -49,6 +49,9 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
 		{
 			Name: "foo",
 
+			Annotations: map[string]string{
+				"com.example.foo": "bar",
+			},
 			Build: &types.BuildConfig{
 				Context:            "./dir",
 				Dockerfile:         "Dockerfile",
@@ -599,6 +602,8 @@ services:
         FROM alpine
         RUN echo "hello" > /world.txt
   foo:
+    annotations:
+      com.example.foo: bar
     build:
       context: ./dir
       dockerfile: Dockerfile
@@ -1145,6 +1150,9 @@ func fullExampleJSON(workingDir, homeDir string) string {
       "entrypoint": null
     },
     "foo": {
+      "annotations": {
+        "com.example.foo": "bar"
+      },
       "build": {
         "context": "./dir",
         "dockerfile": "Dockerfile",
Repository: compose-spec/compose-go
Pull request #398 (2023-05-10 07:55:58)

fix !reset support to override attributes with custom yaml name
After https://github.com/compose-spec/compose-go/pull/392 support for `!reset` can't rely anymore on mapstructure tags to get the yaml name for a go struct field

closes https://github.com/docker/compose/issues/8866


Patch:
diff --git a/loader/null.go b/loader/null.go
index e2a6f19..683a827 100644
--- a/loader/null.go
+++ b/loader/null.go
@@ -133,7 +133,9 @@ func (p *ResetProcessor) applyNullOverrides(val reflect.Value, path tree.Path) e
 			field := typ.Field(i)
 			name := field.Name
 			attr := strings.ToLower(name)
-			if tag := field.Tag.Get("mapstructure"); tag != "" {
+			tag := field.Tag.Get("yaml")
+			tag = strings.Split(tag, ",")[0]
+			if tag != "" && tag != "-" {
 				attr = tag
 			}
 			next := path.Next(attr)


Test patch:
diff --git a/loader/merge_test.go b/loader/merge_test.go
index 9d0d600..7c1e361 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -1374,6 +1374,7 @@ services:
     build:
       context: .
       dockerfile: foo.Dockerfile
+    read_only: true
     environment:
       FOO: BAR
 `
@@ -1382,6 +1383,7 @@ services:
   foo:
     image: foo
     build: !reset  
+    read_only: !reset false
     environment:
       FOO: !reset
 `
@@ -1402,6 +1404,7 @@ services:
 				Name:        "foo",
 				Image:       "foo",
 				Environment: types.MappingWithEquals{},
+				ReadOnly:    false,
 				Scale:       1,
 			},
 		},
Repository: compose-spec/compose-go
Pull request #402 (2023-05-12 13:23:33)

volume can be defined by a numeric ID
close https://github.com/docker/compose/issues/9652


Patch:
diff --git a/loader/volume.go b/loader/volume.go
index f1e66cd..dd83414 100644
--- a/loader/volume.go
+++ b/loader/volume.go
@@ -176,5 +176,8 @@ func isFilePath(source string) bool {
 	}
 
 	first, nextIndex := utf8.DecodeRuneInString(source)
+	if len(source) <= nextIndex {
+		return false
+	}
 	return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
 }


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index cf27efc..1ae80b0 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2354,3 +2354,34 @@ func TestInvalidProjectNameType(t *testing.T) {
 	assert.Error(t, err, "validating filename0.yml: name must be a string")
 	assert.Assert(t, is.Nil(p))
 }
+
+func TestNumericIDs(t *testing.T) {
+	p, err := loadYAML(`
+name: 'test-numeric-ids'
+services:
+  foo:
+    image: busybox
+    volumes:
+      - 0:/foo
+
+volumes:
+  '0': {}
+`)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services, types.Services{
+		{
+			Name:        "foo",
+			Image:       "busybox",
+			Environment: types.MappingWithEquals{},
+			Scale:       1,
+			Volumes: []types.ServiceVolumeConfig{
+				{
+					Type:   types.VolumeTypeVolume,
+					Source: "0",
+					Target: "/foo",
+					Volume: &types.ServiceVolumeVolume{},
+				},
+			},
+		},
+	})
+}