fix(copilot): hint to run copilot directly when exec fails
When the copilot binary is found in PATH but exec fails (e.g., due to unusual characters like parentheses in the path on Windows), append a hint suggesting the user run `copilot` directly without `gh`. The hint is only shown when the binary was already present on the host, not when it was freshly downloaded and installed by `gh`. Closes cli/cli#13106 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
9b505c3fb8
commit
601dd346b0
2 changed files with 45 additions and 3 deletions
|
|
@ -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 {
|
||||
// The binary exists in PATH 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\nTry running `copilot` directly without `gh`.", err)
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue