From edc2339f6f2fa94156f01d09416b8bbea835d9a1 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Thu, 17 Sep 2026 22:16:06 +0530 Subject: [PATCH] fix: allow disabled services as build additional contexts Validate build-only dependencies without enabling their profiles. Reproduces docker/compose#14223 through the loader and preserves missing and non-buildable target errors. Assisted-by: OpenAI Codex Signed-off-by: Shubham Padkonde --- loader/tests/build_test.go | 50 ++++++++++++++++++++++++++++++++++++++ loader/validate.go | 4 +++ 2 files changed, 54 insertions(+) diff --git a/loader/tests/build_test.go b/loader/tests/build_test.go index 20e572e65..1b82e797e 100644 --- a/loader/tests/build_test.go +++ b/loader/tests/build_test.go @@ -24,12 +24,62 @@ package tests // source." import ( + "context" "testing" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" "gotest.tools/v3/assert" ) +// A service additional context may refer to a build-only service in an inactive +// profile without enabling that service for execution. +// https://github.com/compose-spec/compose-spec/blob/main/build.md#additional_contexts +// Regression: https://github.com/docker/compose/issues/14223. +func TestBuildAdditionalContextDisabledService(t *testing.T) { + for _, tc := range []struct { + name string + base string + wantError string + }{ + {"buildable", "build: .", ""}, + {"image only", "image: busybox", "non-buildable service"}, + {"missing", "", "unknown service"}, + } { + t.Run(tc.name, func(t *testing.T) { + content := ` +name: test +services: + classroom: + profiles: [classroom] + build: + context: . + additional_contexts: + base: service:base +` + if tc.base != "" { + content += " base:\n profiles: [disabled_services]\n " + tc.base + "\n" + } + p, err := loader.LoadWithContext(context.Background(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(content)}}, + }, func(options *loader.Options) { + options.Profiles = []string{"classroom"} + }) + if tc.wantError != "" { + assert.ErrorContains(t, err, tc.wantError) + assert.ErrorContains(t, err, `service "classroom"`) + assert.ErrorContains(t, err, `"base" as additional contexts base`) + return + } + assert.NilError(t, err) + assert.Equal(t, len(p.Services), 1) + assert.Equal(t, p.Services["classroom"].Build.AdditionalContexts["base"], "service:base") + assert.Equal(t, len(p.DisabledServices), 1) + assert.Assert(t, p.DisabledServices["base"].Build != nil) + }) + } +} + func TestBuildConfig(t *testing.T) { p := load(t, ` name: test diff --git a/loader/validate.go b/loader/validate.go index 4ac4c2dd5..a04a067f4 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -41,6 +41,10 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo for add, c := range s.Build.AdditionalContexts { if target, ok := strings.CutPrefix(c, types.ServicePrefix); ok { t, err := project.GetService(target) + if errors.Is(err, errdefs.ErrDisabled) { + t = project.DisabledServices[target] + err = nil + } if err != nil { return fmt.Errorf("service %q declares unknown service %q as additional contexts %s", name, target, add) }