Skip to content
Open
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
6 changes: 5 additions & 1 deletion pkg/api/client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ func resolveOptions(opts ClientOptions) (ClientOptions, error) {
opts.Host, _ = auth.DefaultHost()
}
if opts.AuthToken == "" {
opts.AuthToken, _ = auth.TokenForHost(opts.Host)
source := ""
opts.AuthToken, source = auth.TokenForHost(opts.Host)
if opts.AuthToken == "" {
if source != "" && source != "default" {
return ClientOptions{}, fmt.Errorf("authentication token not found for host %s: %s", opts.Host, source)
}
return ClientOptions{}, fmt.Errorf("authentication token not found for host %s", opts.Host)
}
}
Expand Down
15 changes: 15 additions & 0 deletions pkg/api/client_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ func TestResolveOptions(t *testing.T) {
}
}

func TestResolveOptions_ReturnsTokenLookupFailureReason(t *testing.T) {
testutils.StubConfig(t, "")
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "")
t.Setenv("GH_ENTERPRISE_TOKEN", "")
t.Setenv("GITHUB_ENTERPRISE_TOKEN", "")
t.Setenv("GH_PATH", "/path/to/missing/gh")

_, err := resolveOptions(ClientOptions{Host: "github.com"})

assert.Error(t, err)
assert.Contains(t, err.Error(), "authentication token not found for host github.com")
assert.Contains(t, err.Error(), "failed to run `gh auth token`")
}

func TestOptionsNeedResolution(t *testing.T) {
tests := []struct {
name string
Expand Down
29 changes: 21 additions & 8 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const (
// host. The source can be either an environment variable, configuration file, or the system
// keyring. In the latter case, this shells out to "gh auth token" to obtain the token.
//
// Returns "", "default" if no applicable token is found.
// Returns an empty token and a source that can either be "default" or a reason why lookup failed.
func TokenForHost(host string) (string, string) {
if token, source := TokenFromEnvOrConfig(host); token != "" {
return token, source
Expand All @@ -43,10 +43,14 @@ func TokenForHost(host string) (string, string) {
ghExe, _ = safeexec.LookPath("gh")
}

if ghExe != "" {
if token, source := tokenFromGh(ghExe, host); token != "" {
return token, source
}
if ghExe == "" {
return "", "gh executable not found in PATH"
}

if token, source := tokenFromGh(ghExe, host); token != "" {
return token, source
} else if source != "" {
return "", source
}

return "", defaultSource
Expand Down Expand Up @@ -101,11 +105,20 @@ func tokenForHost(cfg *config.Config, host string) (string, string) {

func tokenFromGh(path string, host string) (string, string) {
cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host)
result, err := cmd.Output()
result, err := cmd.CombinedOutput()
if err != nil {
return "", "gh"
details := strings.TrimSpace(string(result))
if details == "" {
details = err.Error()
}
return "", fmt.Sprintf("failed to run `gh auth token`: %s", details)
}

token := strings.TrimSpace(string(result))
if token == "" {
return "", "gh did not return an authentication token"
}
return strings.TrimSpace(string(result)), "gh"
return token, "gh"
}

// KnownHosts retrieves a list of hosts that have corresponding
Expand Down
18 changes: 16 additions & 2 deletions pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"testing"

"github.com/cli/go-gh/v2/internal/testutils"
"github.com/cli/go-gh/v2/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -147,6 +148,16 @@ func TestTokenForHost(t *testing.T) {
}
}

func TestTokenForHost_ReturnsReasonWhenGhCommandFails(t *testing.T) {
testutils.StubConfig(t, testNoHostsConfigData())
t.Setenv("GH_PATH", "/path/to/missing/gh")

token, source := TokenForHost("github.com")

assert.Equal(t, "", token)
assert.Contains(t, source, "failed to run `gh auth token`")
}

func TestDefaultHost(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -399,8 +410,11 @@ func TestNormalizeHostname(t *testing.T) {
}

func testNoHostsConfig() *config.Config {
var data = ``
return config.ReadFromString(data)
return config.ReadFromString(testNoHostsConfigData())
}

func testNoHostsConfigData() string {
return ``
}

func testSingleHostConfig() *config.Config {
Expand Down