From 6cbb80d8130431d5078cae9a403a757879d77f62 Mon Sep 17 00:00:00 2001 From: Michael Irwin Date: Thu, 5 Feb 2026 12:03:38 -0500 Subject: [PATCH] Fix invalid path error when using OCI artifacts on Windows When using OCI artifacts (e.g., `docker compose -f oci://dockersamples/welcome-to-docker up`) on Windows, users encountered the following error: CreateFile C:\Users\username\oci:\dockersamples\.env: The filename, directory name, or volume label syntax is incorrect. This issue was introduced between v5.0.0 and v5.0.1, specifically by commit 6c043929a which fixed error handling in setEnvWithDotEnv. The bug existed in v5.0.0 but was silently ignored due to improper error handling. Root Cause: ----------- The setEnvWithDotEnv function creates ProjectOptions without registering remote loaders. Without remote loaders, the compose-go library doesn't recognize OCI paths as remote resources. It falls through to filepath.Abs() which treats the OCI reference as a relative path. On Windows, filepath.Abs("oci://dockersamples/...") produces an invalid path like: C:\Users\username\oci:\dockersamples Windows rejects this path because colons are only valid after drive letters. Solution: --------- Modified setEnvWithDotEnv to detect remote config paths and skip environment loading for them. Instead of hardcoding string checks, the fix uses the actual remote loaders' Accept() method to determine if a config path is remote. This is more maintainable and consistent with how the compose-go library identifies remote resources. The function now: - Accepts a dockerCli parameter to access remote loaders - Uses opts.remoteLoaders(dockerCli) to get loader instances - Checks if any loader accepts the config path using loader.Accept() - Skips .env loading for remote configs (happens later when loaders are initialized) - Allows normal processing for local compose files Testing: -------- - Added tests for OCI artifacts, Git remotes, and local paths - Verified fix works on Windows ARM64 - All existing tests pass Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Michael Irwin --- cmd/compose/compose.go | 18 +++++++- cmd/compose/compose_oci_test.go | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 cmd/compose/compose_oci_test.go diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 238acdd602..4adc460de1 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -477,7 +477,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C logrus.SetLevel(logrus.TraceLevel) } - err := setEnvWithDotEnv(opts) + err := setEnvWithDotEnv(opts, dockerCli) if err != nil { return err } @@ -677,7 +677,21 @@ func stdinfo(dockerCli command.Cli) io.Writer { return dockerCli.Err() } -func setEnvWithDotEnv(opts ProjectOptions) error { +func setEnvWithDotEnv(opts ProjectOptions, dockerCli command.Cli) error { + // Check if we're using a remote config (OCI or Git) + // If so, skip env loading as remote loaders haven't been initialized yet + // and trying to process the path would fail + remoteLoaders := opts.remoteLoaders(dockerCli) + for _, path := range opts.ConfigPaths { + for _, loader := range remoteLoaders { + if loader.Accept(path) { + // Remote config - skip env loading for now + // It will be loaded later when the project is fully initialized + return nil + } + } + } + options, err := cli.NewProjectOptions(opts.ConfigPaths, cli.WithWorkingDirectory(opts.ProjectDir), cli.WithOsEnv, diff --git a/cmd/compose/compose_oci_test.go b/cmd/compose/compose_oci_test.go new file mode 100644 index 0000000000..7450f7633c --- /dev/null +++ b/cmd/compose/compose_oci_test.go @@ -0,0 +1,76 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "testing" + + "go.uber.org/mock/gomock" + "gotest.tools/v3/assert" + + "github.com/docker/compose/v5/pkg/mocks" +) + +func TestSetEnvWithDotEnv_WithOCIArtifact(t *testing.T) { + // Test that setEnvWithDotEnv doesn't fail when using OCI artifacts + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cli := mocks.NewMockCli(ctrl) + + opts := ProjectOptions{ + ConfigPaths: []string{"oci://docker.io/dockersamples/welcome-to-docker"}, + ProjectDir: "", + EnvFiles: []string{}, + } + + err := setEnvWithDotEnv(opts, cli) + assert.NilError(t, err, "setEnvWithDotEnv should not fail with OCI artifact path") +} + +func TestSetEnvWithDotEnv_WithGitRemote(t *testing.T) { + // Test that setEnvWithDotEnv doesn't fail when using Git remotes + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cli := mocks.NewMockCli(ctrl) + + opts := ProjectOptions{ + ConfigPaths: []string{"https://github.com/docker/compose.git"}, + ProjectDir: "", + EnvFiles: []string{}, + } + + err := setEnvWithDotEnv(opts, cli) + assert.NilError(t, err, "setEnvWithDotEnv should not fail with Git remote path") +} + +func TestSetEnvWithDotEnv_WithLocalPath(t *testing.T) { + // Test that setEnvWithDotEnv still works with local paths + // This will fail if the file doesn't exist, but it should not panic + // or produce invalid paths + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cli := mocks.NewMockCli(ctrl) + + opts := ProjectOptions{ + ConfigPaths: []string{"compose.yaml"}, + ProjectDir: "", + EnvFiles: []string{}, + } + + // This may error if files don't exist, but should not panic + _ = setEnvWithDotEnv(opts, cli) +}