From 94fea503d6c08fe1ffa839e55919c23e0147eec9 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Thu, 20 Aug 2026 21:09:26 +0200 Subject: [PATCH] fix(build): honor provenance/sbom false in per-service bake attest When a service set `build.provenance: false` (or `sbom: false`), the bake attest entry was silently omitted (the field is `omitempty`), so BuildKit applied its defaults and attached a provenance attestation anyway. The attestation changes the manifest-list digest on every build, which caused containers to be needlessly recreated on `up --build`. Emit `"type=,disabled=true"` instead, which BuildKit's `Attest.UnmarshalText` parses as `Disabled:true` and forwards to BuildKit as an explicit disable signal. Extract `appendAttest` to avoid duplicating the three-branch switch for both provenance and sbom. Fixes #14111 Signed-off-by: Guillaume Lours --- pkg/compose/build_bake.go | 30 +++++++++------------ pkg/compose/build_bake_test.go | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 31e2a58c5d..87fce90779 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -599,25 +599,21 @@ func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig) func toBakeAttest(buildConfig types.BuildConfig) []string { var attests []string + attests = appendAttest(attests, "provenance", buildConfig.Provenance) + attests = appendAttest(attests, "sbom", buildConfig.SBOM) + return attests +} - // Handle per-service provenance configuration (only from build config, not global options) - if buildConfig.Provenance != "" { - if buildConfig.Provenance == "true" { - attests = append(attests, "type=provenance") - } else if buildConfig.Provenance != "false" { - attests = append(attests, fmt.Sprintf("type=provenance,%s", buildConfig.Provenance)) - } - } - - // Handle per-service SBOM configuration (only from build config, not global options) - if buildConfig.SBOM != "" { - if buildConfig.SBOM == "true" { - attests = append(attests, "type=sbom") - } else if buildConfig.SBOM != "false" { - attests = append(attests, fmt.Sprintf("type=sbom,%s", buildConfig.SBOM)) - } +func appendAttest(attests []string, attestType, value string) []string { + switch value { + case "": + case "true": + attests = append(attests, "type="+attestType) + case "false": + attests = append(attests, "type="+attestType+",disabled=true") + default: + attests = append(attests, fmt.Sprintf("type=%s,%s", attestType, value)) } - return attests } diff --git a/pkg/compose/build_bake_test.go b/pkg/compose/build_bake_test.go index b48b7ead45..ee814c2362 100644 --- a/pkg/compose/build_bake_test.go +++ b/pkg/compose/build_bake_test.go @@ -51,6 +51,55 @@ func TestBakeTargetNames(t *testing.T) { }) } +func TestToBakeAttest(t *testing.T) { + tests := []struct { + name string + config types.BuildConfig + expected []string + }{ + { + name: "empty — no attest entries", + config: types.BuildConfig{}, + expected: nil, + }, + { + name: "provenance true", + config: types.BuildConfig{Provenance: "true"}, + expected: []string{"type=provenance"}, + }, + { + name: "provenance false — must disable, not omit", + config: types.BuildConfig{Provenance: "false"}, + expected: []string{"type=provenance,disabled=true"}, + }, + { + name: "provenance mode=max", + config: types.BuildConfig{Provenance: "mode=max"}, + expected: []string{"type=provenance,mode=max"}, + }, + { + name: "sbom true", + config: types.BuildConfig{SBOM: "true"}, + expected: []string{"type=sbom"}, + }, + { + name: "sbom false — must disable, not omit", + config: types.BuildConfig{SBOM: "false"}, + expected: []string{"type=sbom,disabled=true"}, + }, + { + name: "provenance false + sbom false", + config: types.BuildConfig{Provenance: "false", SBOM: "false"}, + expected: []string{"type=provenance,disabled=true", "type=sbom,disabled=true"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.DeepEqual(t, toBakeAttest(tc.config), tc.expected) + }) + } +} + // makeConsole must hand the genuine *os.File over when the stream wraps one: // on Windows, containerd/console rejects anything but the exact // os.Stdin/Stdout/Stderr values, so a wrapper would disable the TTY progress