diff --git a/cmd/gh/main.go b/cmd/gh/main.go index 6870c9079..456195314 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -10,6 +10,7 @@ import ( "path" "strings" + "github.com/cli/cli/api" "github.com/cli/cli/command" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghinstance" @@ -91,10 +92,41 @@ func main() { } } + authCheckEnabled := cmdutil.IsAuthCheckEnabled(cmd) + + // TODO support other names + ghtoken := os.Getenv("GITHUB_TOKEN") + if ghtoken != "" { + authCheckEnabled = false + } + + if authCheckEnabled { + hasAuth := false + + cfg, err := cmdFactory.Config() + if err == nil { + hasAuth = cmdutil.CheckAuth(cfg) + } + + if !hasAuth { + fmt.Fprintln(stderr, utils.Bold("Welcome to GitHub CLI!")) + fmt.Fprintln(stderr) + fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.") + fmt.Fprintln(stderr, "You can also set the GITHUB_TOKEN environment variable, if preferred.") + os.Exit(4) + } + } + rootCmd.SetArgs(expandedArgs) if cmd, err := rootCmd.ExecuteC(); err != nil { printError(stderr, err, cmd, hasDebug) + + var httpErr api.HTTPError + if errors.As(err, &httpErr) && httpErr.StatusCode == 401 { + fmt.Println("hint: try authenticating with `gh auth login`") + } + os.Exit(1) } if root.HasFailed() { diff --git a/pkg/cmd/alias/alias.go b/pkg/cmd/alias/alias.go index 20bc26e83..0a2971f04 100644 --- a/pkg/cmd/alias/alias.go +++ b/pkg/cmd/alias/alias.go @@ -20,6 +20,8 @@ func NewCmdAlias(f *cmdutil.Factory) *cobra.Command { `), } + cmdutil.DisableAuthCheck(cmd) + cmd.AddCommand(deleteCmd.NewCmdDelete(f, nil)) cmd.AddCommand(listCmd.NewCmdList(f, nil)) cmd.AddCommand(setCmd.NewCmdSet(f, nil)) diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index 67c6abb31..ddeb07aa6 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -16,6 +16,8 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { Long: `Manage gh's authentication state.`, } + cmdutil.DisableAuthCheck(cmd) + cmd.AddCommand(authLoginCmd.NewCmdLogin(f, nil)) cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil)) cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil)) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 7d53839c6..597bc910b 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -54,7 +54,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm # => read token from mytoken.txt and authenticate against github.com $ gh auth login --hostname enterprise.internal --with-token < mytoken.txt - # => read token from mytoken.txt and authenticate against a GitHub Enterprise instance + # => read token from mytoken.txt and authenticate against a GitHub Enterprise Server instance `), RunE: func(cmd *cobra.Command, args []string) error { isTTY := opts.IO.IsStdinTTY() @@ -142,7 +142,7 @@ func loginRun(opts *LoginOptions) error { Message: "What account do you want to log into?", Options: []string{ "GitHub.com", - "GitHub Enterprise", + "GitHub Enterprise Server", }, }, &hostType) diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index b92af7a88..3fa86ddcb 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -21,6 +21,8 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { `), } + cmdutil.DisableAuthCheck(cmd) + cmd.AddCommand(NewCmdConfigGet(f)) cmd.AddCommand(NewCmdConfigSet(f)) diff --git a/pkg/cmd/factory/http.go b/pkg/cmd/factory/http.go index b113100f7..1cccfda9c 100644 --- a/pkg/cmd/factory/http.go +++ b/pkg/cmd/factory/http.go @@ -1,7 +1,6 @@ package factory import ( - "errors" "fmt" "net/http" "os" @@ -30,20 +29,9 @@ func httpClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, s hostname := ghinstance.NormalizeHostname(req.URL.Hostname()) token, err := cfg.Get(hostname, "oauth_token") - if token == "" { - var notFound *config.NotFoundError - // TODO: check if stdout is TTY too - if errors.As(err, ¬Found) && io.IsStdinTTY() { - // interactive OAuth flow - token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required", nil) - } - if err != nil { - return "", err - } - if token == "" { - // TODO: instruct user how to manually authenticate - return "", fmt.Errorf("authentication required for %s", hostname) - } + if err != nil || token == "" { + // Users shouldn't see this because of the pre-execute auth check on commands + return "", fmt.Errorf("authentication required for %s; please run `gh auth login -h %s`", hostname, hostname) } return fmt.Sprintf("token %s", token), nil diff --git a/pkg/cmd/factory/remote_resolver.go b/pkg/cmd/factory/remote_resolver.go index 0df3e0ca8..589b19959 100644 --- a/pkg/cmd/factory/remote_resolver.go +++ b/pkg/cmd/factory/remote_resolver.go @@ -72,7 +72,7 @@ func (rr *remoteResolver) Resolver() func() (context.Remotes, error) { } if len(cachedRemotes) == 0 { - remotesError = errors.New("none of the git remotes point to a known GitHub host") + remotesError = errors.New("none of the git remotes configured for this repository point to a known GitHub host. To tell gh about a new GitHub host, please use `gh auth login`") return nil, remotesError } return cachedRemotes, nil diff --git a/pkg/cmd/root/completion.go b/pkg/cmd/root/completion.go index ed907f6db..a96b69840 100644 --- a/pkg/cmd/root/completion.go +++ b/pkg/cmd/root/completion.go @@ -56,6 +56,8 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { }, } + cmdutil.DisableAuthCheck(cmd) + cmd.Flags().StringVarP(&shellType, "shell", "s", "", "Shell type: {bash|zsh|fish|powershell}") return cmd diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 39c95469d..46ffbef33 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -98,6 +98,8 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { return &cmdutil.FlagError{Err: err} }) + cmdutil.DisableAuthCheck(cmd) + // CHILD COMMANDS cmd.AddCommand(aliasCmd.NewCmdAlias(f)) diff --git a/pkg/cmdutil/auth_check.go b/pkg/cmdutil/auth_check.go new file mode 100644 index 000000000..5d40d0143 --- /dev/null +++ b/pkg/cmdutil/auth_check.go @@ -0,0 +1,46 @@ +package cmdutil + +import ( + "github.com/cli/cli/internal/config" + "github.com/spf13/cobra" +) + +// TODO can have this set a PersistentPreRun so we don't have to set for all child commands of auth, +// config + +func DisableAuthCheck(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + + cmd.Annotations["skipAuthCheck"] = "true" +} + +func CheckAuth(cfg config.Config) bool { + hosts, err := cfg.Hosts() + if err != nil { + return false + } + + for _, hostname := range hosts { + token, _ := cfg.Get(hostname, "oauth_token") + if token != "" { + return true + } + } + + return false +} + +func IsAuthCheckEnabled(cmd *cobra.Command) bool { + if !cmd.Runnable() { + return false + } + for c := cmd; c.Parent() != nil; c = c.Parent() { + if c.Annotations != nil && c.Annotations["skipAuthCheck"] == "true" { + return false + } + } + + return true +} diff --git a/pkg/cmdutil/auth_check_test.go b/pkg/cmdutil/auth_check_test.go new file mode 100644 index 000000000..22b8ff5d4 --- /dev/null +++ b/pkg/cmdutil/auth_check_test.go @@ -0,0 +1,45 @@ +package cmdutil + +import ( + "testing" + + "github.com/cli/cli/internal/config" + "github.com/stretchr/testify/assert" +) + +func Test_CheckAuth(t *testing.T) { + tests := []struct { + name string + cfg func(config.Config) + expected bool + }{ + { + name: "no hosts", + cfg: func(c config.Config) {}, + expected: false, + }, + { + name: "host, no token", + cfg: func(c config.Config) { + _ = c.Set("github.com", "oauth_token", "") + }, + expected: false, + }, + { + name: "host, token", + cfg: func(c config.Config) { + _ = c.Set("github.com", "oauth_token", "a token") + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := config.NewBlankConfig() + tt.cfg(cfg) + result := CheckAuth(cfg) + assert.Equal(t, tt.expected, result) + }) + } +}