diff --git a/pkg/cmd/copilot/copilot.go b/pkg/cmd/copilot/copilot.go index 50b00e9fe..cc83ef48e 100644 --- a/pkg/cmd/copilot/copilot.go +++ b/pkg/cmd/copilot/copilot.go @@ -142,8 +142,9 @@ func runCopilot(opts *CopilotOptions) error { return nil } - copilotPath := findCopilotBinary() - if copilotPath == "" { + copilotPath := findCopilotBinaryFunc() + foundInPath := copilotPath != "" + if !foundInPath { if opts.IO.CanPrompt() { confirmed, err := opts.Prompter.Confirm("GitHub Copilot CLI is not installed. Would you like to install it?", true) if err != nil { @@ -175,12 +176,18 @@ func runCopilot(opts *CopilotOptions) error { externalCmd.Stderr = opts.IO.ErrOut externalCmd.Env = append(os.Environ(), "COPILOT_GH=true") - if err := externalCmd.Run(); err != nil { + if err := runExternalCmdFunc(externalCmd); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { // We terminate with os.Exit here, preserving the exit code from Copilot CLI, // and also preventing stdio writes by callers up the stack. os.Exit(exitErr.ExitCode()) } + if foundInPath { + // We found a `copilot` binary but exec failed, possibly due to + // unusual characters in the path (see https://github.com/cli/cli/issues/13106). + // Suggest running copilot directly as a workaround. + return fmt.Errorf("%w\nFailed to run '%s', try running `copilot` directly without `gh`.", err, copilotPath) + } return err } return nil @@ -200,6 +207,14 @@ func copilotBinaryPath() string { return filepath.Join(copilotInstallDir(), binaryName) } +var runExternalCmdFunc = runExternalCmd + +func runExternalCmd(cmd *exec.Cmd) error { + return cmd.Run() +} + +var findCopilotBinaryFunc = findCopilotBinary + // findCopilotBinary returns the path to the Copilot CLI binary, if installed, // with the following order of precedence: // 1. `copilot` in the PATH diff --git a/pkg/cmd/copilot/copilot_test.go b/pkg/cmd/copilot/copilot_test.go index 07e0191e6..fa173f528 100644 --- a/pkg/cmd/copilot/copilot_test.go +++ b/pkg/cmd/copilot/copilot_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -589,6 +590,32 @@ func TestDownloadCopilot(t *testing.T) { }) } +func TestRunCopilot_execFailureHint(t *testing.T) { + ios, _, _, _ := iostreams.Test() + opts := &CopilotOptions{ + IO: ios, + CopilotArgs: []string{}, + } + + origFind := findCopilotBinaryFunc + findCopilotBinaryFunc = func() string { + return "/usr/bin/copilot" + } + t.Cleanup(func() { findCopilotBinaryFunc = origFind }) + + execErr := fmt.Errorf("exec failed: something went wrong") + origRun := runExternalCmdFunc + runExternalCmdFunc = func(_ *exec.Cmd) error { + return execErr + } + t.Cleanup(func() { runExternalCmdFunc = origRun }) + + err := runCopilot(opts) + require.Error(t, err) + require.ErrorIs(t, err, execErr) + require.Contains(t, err.Error(), "try running `copilot` directly without `gh`.") +} + func TestCopilotCommandIsSampledAt100(t *testing.T) { spy := &telemetry.CommandRecorderSpy{} factory := &cmdutil.Factory{}