Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion cli/azd/cmd/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ func isJsonOutputFromArgs(args []string) bool {
return false
}

// stripCwdFlag removes --cwd/-C and its value from an argument list.
// Core azd already processes -C (creates the directory, changes CWD, sets AZD_CWD
// as an absolute path). Passing the raw flag through to extensions would cause them
// to re-resolve a relative path against the already-changed CWD, doubling the path.
func stripCwdFlag(args []string) []string {
result := make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
arg := args[i]

// --cwd=value or -C=value (combined form with equals)
if strings.HasPrefix(arg, "--cwd=") || strings.HasPrefix(arg, "-C=") {
continue
}

// -Cvalue (combined shorthand without equals, e.g. -CmyFolder)
if len(arg) > 2 && strings.HasPrefix(arg, "-C") && arg[2] != '-' {
continue
}

// --cwd value or -C value (separate value)
if (arg == "--cwd" || arg == "-C") && i+1 < len(args) {
i++ // skip the value
continue
}

result = append(result, arg)
}
return result
}
Comment thread
vhvb1989 marked this conversation as resolved.
Resolved

// bindExtension binds the extension to the root command
func bindExtension(
root *actions.ActionDescriptor,
Expand Down Expand Up @@ -264,8 +294,15 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error
// global flags like --debug, --cwd, or -e. The globalOptions were populated
// by ParseGlobalFlags() before command tree construction and are the only
// reliable source for these values.
//
// Strip --cwd/-C from args: core azd already handled it (created the
// directory, changed CWD, set AZD_CWD to absolute path). Passing the
// raw relative -C value through would cause the extension to resolve
// it again against the already-changed CWD, doubling the path.
extensionArgs := stripCwdFlag(a.args)

options := &extensions.InvokeOptions{
Args: a.args,
Args: extensionArgs,
Env: allEnv,
// cmd extensions are always interactive (connected to terminal)
Interactive: true,
Expand Down
58 changes: 58 additions & 0 deletions cli/azd/cmd/extensions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,61 @@ func TestBindExtension_DeeplyNestedNamespace(t *testing.T) {
require.Equal(t, "Extension for fine tuning AI models.", finetuneCmd.Options.Command.Short)
require.Equal(t, "Extension for evaluating AI models.", evalCmd.Options.Command.Short)
}

func TestStripCwdFlag(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
want []string
}{
{
name: "NoFlag",
args: []string{"init", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "LongFormSeparateValue",
args: []string{"init", "--cwd", "myFolder", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "LongFormEqualsValue",
args: []string{"init", "--cwd=myFolder", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "ShortFormSeparateValue",
args: []string{"init", "-C", "myFolder", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "ShortFormEqualsValue",
args: []string{"init", "-C=myFolder", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "ShortFormCombinedValue",
args: []string{"init", "-CmyFolder", "--no-prompt"},
want: []string{"init", "--no-prompt"},
},
{
name: "EmptyArgs",
args: []string{},
want: []string{},
},
{
name: "OnlyCwdFlag",
args: []string{"-C", "/some/path"},
want: []string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := stripCwdFlag(tt.args)
require.Equal(t, tt.want, got)
})
}
}
16 changes: 16 additions & 0 deletions cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"maps"
"os"
"path/filepath"
"slices"
"strings"

Expand Down Expand Up @@ -95,6 +96,21 @@ func newRootCmd(
}

if opts.Cwd != "" {
// Resolve to absolute path before any usage so downstream
// consumers (error messages, AZD_CWD for extensions) receive
// a canonical path and don't double-resolve relative paths.
absPath, err := filepath.Abs(opts.Cwd)
if err != nil {
return fmt.Errorf("failed to resolve path '%s': %w", opts.Cwd, err)
}
opts.Cwd = absPath

// Update the cobra flag so middleware that reads from
// Flags.GetString("cwd") also sees the absolute path.
if f := cmd.Root().PersistentFlags().Lookup("cwd"); f != nil {
f.Value.Set(absPath) //nolint:errcheck
}

current, err := os.Getwd()

if err != nil {
Expand Down
82 changes: 82 additions & 0 deletions cli/azd/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"os"
"path/filepath"
"testing"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/stretchr/testify/require"
)

func TestRootCmd_CwdRelativePathResolvedToAbsolute(t *testing.T) {
// Regression test: a relative -C path must be resolved to an absolute path
// before os.Chdir so that the AZD_CWD value propagated to extensions doesn't
// get double-resolved (once by the root command, once by the extension).
// See https://github.com/Azure/azure-dev/issues/8229

tmpDir := t.TempDir()
t.Chdir(tmpDir)

relDir := "mySubFolder"
absExpected := filepath.Join(tmpDir, relDir)

container := ioc.NewNestedContainer(nil)
opts := &internal.GlobalCommandOptions{
Cwd: relDir,
NoPrompt: true,
}
ioc.RegisterInstance(container, opts)

rootCmd := NewRootCmd(false, nil, container)

// Add a no-op subcommand so cobra has something to run
rootCmd.SetArgs([]string{"version"})
rootCmd.SetContext(t.Context())

// PersistentPreRunE triggers on Execute
err := rootCmd.Execute()
require.NoError(t, err)

require.Equal(t, absExpected, opts.Cwd,
"relative -C path should be resolved to absolute before chdir")
Comment thread
vhvb1989 marked this conversation as resolved.
Resolved

// Verify the cobra flag was also updated
f := rootCmd.PersistentFlags().Lookup("cwd")
require.NotNil(t, f)
require.Equal(t, absExpected, f.Value.String(),
"cobra cwd flag should be updated to absolute path")

// Note: process CWD is restored by PersistentPostRunE, so we don't check it here.
// The important thing is that opts.Cwd and the cobra flag hold the absolute path,
// which is what gets propagated to extensions as AZD_CWD.
}

func TestRootCmd_CwdAbsolutePathUnchanged(t *testing.T) {
// Absolute paths should pass through unchanged.

tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "absTest")
require.NoError(t, os.MkdirAll(subDir, 0755))

container := ioc.NewNestedContainer(nil)
opts := &internal.GlobalCommandOptions{
Cwd: subDir,
NoPrompt: true,
}
ioc.RegisterInstance(container, opts)

rootCmd := NewRootCmd(false, nil, container)
rootCmd.SetArgs([]string{"version"})
rootCmd.SetContext(t.Context())

err := rootCmd.Execute()
require.NoError(t, err)

require.Equal(t, subDir, opts.Cwd,
"absolute path should remain unchanged")
}
Loading