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=<t>,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 <[email protected]>
This commit is contained in:
Guillaume Lours
2026-08-25 17:46:03 +02:00
committed by Nicolas De loof
parent a472f714d5
commit 03ad3cc1c2
2 changed files with 62 additions and 17 deletions
+13 -17
View File
@@ -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))
}
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))
}
// 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))
}
}
return attests
}
+49
View File
@@ -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