Merge pull request #13068 from 333fred/print-refresh-for-401s

Print `gh auth refresh` for 401 returns
This commit is contained in:
Kynan Ware 2026-05-05 11:43:37 -06:00 committed by GitHub
commit 7c439196c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 108 additions and 5 deletions

View file

@ -26,6 +26,7 @@ import (
"github.com/cli/cli/v2/internal/gh/ghtelemetry"
"github.com/cli/cli/v2/internal/telemetry"
"github.com/cli/cli/v2/internal/update"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/cli/v2/pkg/cmd/root"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -227,7 +228,11 @@ func Main() exitCode {
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
authCommand := "gh auth login"
if cfg, cfgErr := cmdFactory.Config(); cfgErr == nil {
authCommand = authRecoveryCommand(cfg, httpErr)
}
fmt.Fprintf(stderr, "Try authenticating with: %s\n", authCommand)
} else if u := factory.SSOURL(); u != "" {
// handles organization SAML enforcement error
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
@ -291,6 +296,20 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
}
}
func authRecoveryCommand(cfg gh.Config, httpErr api.HTTPError) string {
if httpErr.RequestURL == nil {
return "gh auth login"
}
hostname := ghauth.NormalizeHostname(httpErr.RequestURL.Hostname())
token, source := cfg.Authentication().ActiveToken(hostname)
if shared.AuthTokenRefreshable(token, source) {
return fmt.Sprintf("gh auth refresh -h %s", hostname)
}
return fmt.Sprintf("gh auth login -h %s", hostname)
}
func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
if updaterEnabled == "" || !update.ShouldCheckForUpdate() {
return nil, nil

View file

@ -5,11 +5,15 @@ import (
"errors"
"fmt"
"net"
"net/url"
"testing"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmdutil"
ghAPI "github.com/cli/go-gh/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
@ -511,3 +515,74 @@ func disableColorLabelsConfig() gh.Config {
func enableColorLabelsConfig() gh.Config {
return config.NewFromString("color_labels: enabled")
}
func Test_authRecoveryCommand(t *testing.T) {
tests := []struct {
name string
token string
source string
requestURL string
want string
}{
{
name: "stored oauth token",
token: "gho_abc123",
source: "oauth_token",
requestURL: "https://api.github.com/graphql",
want: "gh auth refresh -h github.com",
},
{
name: "stored pat",
token: "github_pat_abc123",
source: "oauth_token",
requestURL: "https://api.github.com/graphql",
want: "gh auth login -h github.com",
},
{
name: "env token",
token: "gho_abc123",
source: "GH_TOKEN",
requestURL: "https://api.github.com/graphql",
want: "gh auth login -h github.com",
},
{
name: "missing request url",
token: "gho_abc123",
source: "oauth_token",
want: "gh auth login",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authCfg := config.NewBlankConfig().Authentication()
authCfg.SetActiveToken(tt.token, tt.source)
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return authCfg
},
}
var requestURL *url.URL
if tt.requestURL != "" {
var err error
requestURL, err = url.Parse(tt.requestURL)
if err != nil {
t.Fatalf("failed to parse request URL: %v", err)
}
}
httpErr := api.HTTPError{
HTTPError: &ghAPI.HTTPError{
RequestURL: requestURL,
StatusCode: 401,
},
}
got := authRecoveryCommand(cfg, httpErr)
if got != tt.want {
t.Errorf("authRecoveryCommand() = %q, want %q", got, tt.want)
}
})
}
}

View file

@ -6,6 +6,12 @@ import (
"github.com/cli/cli/v2/internal/gh"
)
// AuthTokenRefreshable reports whether the token is stored by gh and can be
// renewed with `gh auth refresh`.
func AuthTokenRefreshable(token, src string) bool {
return token != "" && !strings.HasSuffix(src, "_TOKEN") && strings.HasPrefix(token, "gho_")
}
func AuthTokenWriteable(authCfg gh.AuthConfig, hostname string) (string, bool) {
token, src := authCfg.ActiveToken(hostname)
return src, (token == "" || !strings.HasSuffix(src, "_TOKEN"))

View file

@ -96,6 +96,9 @@ func (e authEntry) String(cs *iostreams.ColorScheme) string {
sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource))
if authTokenWriteable(e.TokenSource) {
loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host)
if shared.AuthTokenRefreshable(e.Token, e.TokenSource) {
loginInstructions = fmt.Sprintf("gh auth refresh -h %s", e.Host)
}
logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.Host, e.Login)
sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions)))
sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions)))

View file

@ -184,7 +184,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
@ -229,7 +229,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
@ -447,7 +447,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: false
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
@ -535,7 +535,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe-2 (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2
`),
},