Repository: compose-spec/compose-go
Pull request #809 (2025-07-17 15:47:06)

introduce provenance and sbom in build section
introduce `provenance` and `sbom` aligned with https://docs.docker.com/build/metadata/attestations/

see https://github.com/docker/compose/pull/13066


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index ffe60a1..606993e 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -123,6 +123,8 @@
                 "no_cache": {"type": ["boolean", "string"], "description": "Do not use cache when building the image."},
                 "additional_contexts": {"$ref": "#/definitions/list_or_dict", "description": "Additional build contexts to use, specified as a map of name to context path or URL."},
                 "network": {"type": "string", "description": "Network mode to use for the build. Options include 'default', 'none', 'host', or a network name."},
+                "provenance": {"type": ["string","boolean"], "description": "Add a provenance attestation"},
+                "sbom": {"type": ["string","boolean"], "description": "Add a SBOM attestation"},
                 "pull": {"type": ["boolean", "string"], "description": "Always attempt to pull a newer version of the image."},
                 "target": {"type": "string", "description": "Build stage to target in a multi-stage Dockerfile."},
                 "shm_size": {"type": ["integer", "string"], "description": "Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes."},
diff --git a/transform/canonical.go b/transform/canonical.go
index 2754ed3..4448495 100644
--- a/transform/canonical.go
+++ b/transform/canonical.go
@@ -17,6 +17,8 @@
 package transform
 
 import (
+	"fmt"
+
 	"github.com/compose-spec/compose-go/v2/tree"
 )
 
@@ -27,6 +29,8 @@ var transformers = map[tree.Path]transformFunc{}
 func init() {
 	transformers["services.*"] = transformService
 	transformers["services.*.build.secrets.*"] = transformFileMount
+	transformers["services.*.build.provenance"] = transformStringOrX
+	transformers["services.*.build.sbom"] = transformStringOrX
 	transformers["services.*.build.additional_contexts"] = transformKeyValue
 	transformers["services.*.depends_on"] = transformDependsOn
 	transformers["services.*.env_file"] = transformEnvFile
@@ -121,3 +125,12 @@ func transformMapping(v map[string]any, p tree.Path, ignoreParseError bool) (map
 	}
 	return v, nil
 }
+
+func transformStringOrX(data any, _ tree.Path, _ bool) (any, error) {
+	switch v := data.(type) {
+	case string:
+		return v, nil
+	default:
+		return fmt.Sprint(v), nil
+	}
+}
diff --git a/types/derived.gen.go b/types/derived.gen.go
index 93adf49..0225106 100644
--- a/types/derived.gen.go
+++ b/types/derived.gen.go
@@ -875,6 +875,8 @@ func deriveDeepCopy_6(dst, src *BuildConfig) {
 	} else {
 		dst.Args = nil
 	}
+	dst.Provenance = src.Provenance
+	dst.SBOM = src.SBOM
 	if src.SSH == nil {
 		dst.SSH = nil
 	} else {
diff --git a/types/types.go b/types/types.go
index e93d493..d3f4d45 100644
--- a/types/types.go
+++ b/types/types.go
@@ -309,6 +309,8 @@ type BuildConfig struct {
 	DockerfileInline   string                    `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"`
 	Entitlements       []string                  `yaml:"entitlements,omitempty" json:"entitlements,omitempty"`
 	Args               MappingWithEquals         `yaml:"args,omitempty" json:"args,omitempty"`
+	Provenance         string                    `yaml:"provenance,omitempty" json:"provenance,omitempty"`
+	SBOM               string                    `yaml:"sbom,omitempty" json:"sbom,omitempty"`
 	SSH                SSHConfig                 `yaml:"ssh,omitempty" json:"ssh,omitempty"`
 	Labels             Labels                    `yaml:"labels,omitempty" json:"labels,omitempty"`
 	CacheFrom          StringList                `yaml:"cache_from,omitempty" json:"cache_from,omitempty"`


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 548f464..3262869 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3900,3 +3900,19 @@ models:
 		},
 	})
 }
+
+func TestAttestations(t *testing.T) {
+	p, err := loadYAML(`
+name: attestations
+services:
+  test:
+    build:
+      context: .
+      provenance: mode=max
+      sbom: true
+`)
+	assert.NilError(t, err)
+	build := p.Services["test"].Build
+	assert.Equal(t, build.Provenance, "mode=max")
+	assert.Equal(t, build.SBOM, "true")
+}