From c62de09924921230ff8e8743400c002c5df35c7a Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 7 May 2026 08:01:01 -0500 Subject: [PATCH 1/2] fix(config): make shutdownAction defaults explicit in config resolution Apply spec-compliant defaults at the end of MergeConfiguration when shutdownAction is unset: "stopCompose" for Docker Compose configs, "stopContainer" for image/Dockerfile configs. Preserves explicit config values that were being lost when image metadata had no override. --- pkg/devcontainer/config/merge.go | 11 ++++++ pkg/devcontainer/config/merge_test.go | 48 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/pkg/devcontainer/config/merge.go b/pkg/devcontainer/config/merge.go index 1bc50d9dd..b64d38e2a 100644 --- a/pkg/devcontainer/config/merge.go +++ b/pkg/devcontainer/config/merge.go @@ -140,6 +140,17 @@ func MergeConfiguration( ) mergedConfig.HostRequirements = mergeHostRequirements(reversed) + if mergedConfig.ShutdownAction == "" { + switch { + case copiedConfig.ShutdownAction != "": + mergedConfig.ShutdownAction = copiedConfig.ShutdownAction + case len(copiedConfig.DockerComposeFile) > 0: + mergedConfig.ShutdownAction = ShutdownActionStopCompose + default: + mergedConfig.ShutdownAction = ShutdownActionStopContainer + } + } + return mergedConfig, nil } diff --git a/pkg/devcontainer/config/merge_test.go b/pkg/devcontainer/config/merge_test.go index 3c45355d8..671034eed 100644 --- a/pkg/devcontainer/config/merge_test.go +++ b/pkg/devcontainer/config/merge_test.go @@ -516,3 +516,51 @@ func TestMergeLifestyleHooks_MultipleFeatures(t *testing.T) { t.Errorf("expected image hook last, got %v", got[2]) } } + +func TestMergeConfiguration_ShutdownActionDefault_ImageConfig(t *testing.T) { + cfg := &DevContainerConfig{ + ImageContainer: ImageContainer{Image: "ubuntu:latest"}, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionStopContainer { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionStopContainer) + } +} + +func TestMergeConfiguration_ShutdownActionDefault_ComposeConfig(t *testing.T) { + cfg := &DevContainerConfig{ + ComposeContainer: ComposeContainer{ + DockerComposeFile: []string{"docker-compose.yml"}, + Service: "app", + }, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionStopCompose { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionStopCompose) + } +} + +func TestMergeConfiguration_ShutdownActionExplicit_NotOverridden(t *testing.T) { + cfg := &DevContainerConfig{ + DevContainerConfigBase: DevContainerConfigBase{ + ShutdownAction: ShutdownActionNone, + }, + ComposeContainer: ComposeContainer{ + DockerComposeFile: []string{"docker-compose.yml"}, + Service: "app", + }, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionNone { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionNone) + } +} From c496f35a35510041a60b14b9a0afecdcca611f82 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 11 May 2026 15:13:48 -0500 Subject: [PATCH 2/2] test(e2e): add shutdownAction default e2e tests Add read-configuration e2e tests verifying shutdownAction defaults: - Image-based config defaults to "stopContainer" - Docker Compose config defaults to "stopCompose" - Explicit "none" value is preserved unchanged --- .../readconfiguration/readconfiguration.go | 42 +++++++++++++++++++ .../.devcontainer.json | 4 ++ .../docker-compose.yml | 4 ++ .../.devcontainer.json | 4 ++ .../.devcontainer.json | 3 ++ 5 files changed, 57 insertions(+) create mode 100644 e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json create mode 100644 e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml create mode 100644 e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json create mode 100644 e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json diff --git a/e2e/tests/readconfiguration/readconfiguration.go b/e2e/tests/readconfiguration/readconfiguration.go index 0070be1ec..db64eafe4 100644 --- a/e2e/tests/readconfiguration/readconfiguration.go +++ b/e2e/tests/readconfiguration/readconfiguration.go @@ -470,4 +470,46 @@ var _ = ginkgo.Describe("read-configuration command", ginkgo.Label("read-configu }) framework.ExpectError(err) }, ginkgo.SpecTimeout(framework.TimeoutShort())) + + ginkgo.DescribeTable("resolves shutdownAction per devcontainer spec", + ginkgo.Label("read-configuration"), + func(ctx context.Context, testdataDir, expected string) { + f := framework.NewDefaultFramework(initialDir + "/bin") + tempDir, err := framework.CopyToTempDirWithoutChdir(testdataDir) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func() { _ = os.RemoveAll(tempDir) }) + + stdout, _, err := f.ExecCommandCapture(ctx, []string{ + "read-configuration", + "--workspace-folder", tempDir, + "--include-merged-configuration", + }) + framework.ExpectNoError(err) + + var result map[string]any + err = json.Unmarshal([]byte(stdout), &result) + framework.ExpectNoError(err) + + merged, ok := result["mergedConfiguration"].(map[string]any) + gomega.Expect(ok).To(gomega.BeTrue(), "mergedConfiguration should be an object") + gomega.Expect(merged).To( + gomega.HaveKeyWithValue("shutdownAction", expected), + ) + }, + ginkgo.Entry("defaults to stopContainer for image-based config", + "tests/readconfiguration/testdata-shutdown-action", + "stopContainer", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ginkgo.Entry("defaults to stopCompose for docker-compose config", + "tests/readconfiguration/testdata-shutdown-action-compose", + "stopCompose", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ginkgo.Entry("preserves explicit none", + "tests/readconfiguration/testdata-shutdown-action-explicit", + "none", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ) }) diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json new file mode 100644 index 000000000..e8ca08b2d --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "dockerComposeFile": "docker-compose.yml", + "service": "app" +} diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml new file mode 100644 index 000000000..8fd583d54 --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml @@ -0,0 +1,4 @@ +version: "3" +services: + app: + image: ghcr.io/devsy-org/test-images/base:ubuntu diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json new file mode 100644 index 000000000..8d230561b --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ghcr.io/devsy-org/test-images/base:ubuntu", + "shutdownAction": "none" +} diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json new file mode 100644 index 000000000..351e8dcfe --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "ghcr.io/devsy-org/test-images/base:ubuntu" +}