From a79a0bbfd746566de191cf28cf542ada25c9ef26 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 9 Nov 2020 15:02:31 +0300 Subject: [PATCH] Add support for GH_TOKEN and GH_ENTERPRISE_TOKEN --- cmd/gh/main.go | 24 ++-- internal/config/from_env.go | 41 ++++--- internal/config/from_env_test.go | 198 ++++++++++++++++++++++++++----- pkg/cmd/api/api.go | 4 +- pkg/cmd/root/help_topic.go | 8 +- 5 files changed, 215 insertions(+), 60 deletions(-) diff --git a/cmd/gh/main.go b/cmd/gh/main.go index bf4a77da9..e812feae4 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -136,17 +136,15 @@ func main() { cs := cmdFactory.IOStreams.ColorScheme() - authCheckEnabled := os.Getenv("GITHUB_TOKEN") == "" && - os.Getenv("GITHUB_ENTERPRISE_TOKEN") == "" && - cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) - if authCheckEnabled { - if !cmdutil.CheckAuth(cfg) { - fmt.Fprintln(stderr, cs.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) - } + // _, _, tokenPresent := config.AuthTokenFromEnv(ghinstance.OverridableDefault()) + // authCheckEnabled := !tokenPresent && cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) + authCheckEnabled := cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) + if authCheckEnabled && !cmdutil.CheckAuth(cfg) { + fmt.Fprintln(stderr, cs.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 one of the oauth token environment variables, if preferred.") + os.Exit(4) } rootCmd.SetArgs(expandedArgs) @@ -248,8 +246,8 @@ func basicClient(currentVersion string) (*api.Client, error) { } opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion))) - token := os.Getenv("GITHUB_TOKEN") - if token == "" { + token, _, ok := config.AuthTokenFromEnv(ghinstance.Default()) + if !ok { if c, err := config.ParseDefaultConfig(); err == nil { token, _ = c.Get(ghinstance.Default(), "oauth_token") } diff --git a/internal/config/from_env.go b/internal/config/from_env.go index fa930da06..2fb5f75fc 100644 --- a/internal/config/from_env.go +++ b/internal/config/from_env.go @@ -8,7 +8,9 @@ import ( ) const ( + GH_TOKEN = "GH_TOKEN" GITHUB_TOKEN = "GITHUB_TOKEN" + GH_ENTERPRISE_TOKEN = "GH_ENTERPRISE_TOKEN" GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN" ) @@ -28,7 +30,8 @@ func (c *envConfig) Hosts() ([]string, error) { hasDefault = true } } - if (err != nil || !hasDefault) && os.Getenv(GITHUB_TOKEN) != "" { + _, _, found := AuthTokenFromEnv(ghinstance.Default()) + if (err != nil || !hasDefault) && found { hosts = append([]string{ghinstance.Default()}, hosts...) return hosts, nil } @@ -42,13 +45,8 @@ func (c *envConfig) Get(hostname, key string) (string, error) { func (c *envConfig) GetWithSource(hostname, key string) (string, string, error) { if hostname != "" && key == "oauth_token" { - envName := GITHUB_TOKEN - if ghinstance.IsEnterprise(hostname) { - envName = GITHUB_ENTERPRISE_TOKEN - } - - if value := os.Getenv(envName); value != "" { - return value, envName, nil + if value, name, found := AuthTokenFromEnv(hostname); found { + return value, name, nil } } @@ -57,15 +55,28 @@ func (c *envConfig) GetWithSource(hostname, key string) (string, string, error) func (c *envConfig) CheckWriteable(hostname, key string) error { if hostname != "" && key == "oauth_token" { - envName := GITHUB_TOKEN - if ghinstance.IsEnterprise(hostname) { - envName = GITHUB_ENTERPRISE_TOKEN - } - - if os.Getenv(envName) != "" { - return fmt.Errorf("read-only token in %s cannot be modified", envName) + if _, name, found := AuthTokenFromEnv(hostname); found { + return fmt.Errorf("read-only token in %s cannot be modified", name) } } return c.Config.CheckWriteable(hostname, key) } + +func AuthTokenFromEnv(hostname string) (string, string, bool) { + if ghinstance.IsEnterprise(hostname) { + if token, found := os.LookupEnv(GH_ENTERPRISE_TOKEN); found { + return token, GH_ENTERPRISE_TOKEN, found + } + + token, found := os.LookupEnv(GITHUB_ENTERPRISE_TOKEN) + return token, GITHUB_ENTERPRISE_TOKEN, found + } + + if token, found := os.LookupEnv(GH_TOKEN); found { + return token, GH_TOKEN, found + } + + token, found := os.LookupEnv(GITHUB_TOKEN) + return token, GITHUB_TOKEN, found +} diff --git a/internal/config/from_env_test.go b/internal/config/from_env_test.go index 412d37248..55a45f9a0 100644 --- a/internal/config/from_env_test.go +++ b/internal/config/from_env_test.go @@ -11,9 +11,13 @@ import ( func TestInheritEnv(t *testing.T) { orig_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN") orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN") + orig_GH_TOKEN := os.Getenv("GH_TOKEN") + orig_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN") t.Cleanup(func() { os.Setenv("GITHUB_TOKEN", orig_GITHUB_TOKEN) os.Setenv("GITHUB_ENTERPRISE_TOKEN", orig_GITHUB_ENTERPRISE_TOKEN) + os.Setenv("GH_TOKEN", orig_GH_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", orig_GH_ENTERPRISE_TOKEN) }) type wants struct { @@ -28,15 +32,15 @@ func TestInheritEnv(t *testing.T) { baseConfig string GITHUB_TOKEN string GITHUB_ENTERPRISE_TOKEN string + GH_TOKEN string + GH_ENTERPRISE_TOKEN string hostname string wants wants }{ { - name: "blank", - baseConfig: ``, - GITHUB_TOKEN: "", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + name: "blank", + baseConfig: ``, + hostname: "github.com", wants: wants{ hosts: []string(nil), token: "", @@ -45,11 +49,10 @@ func TestInheritEnv(t *testing.T) { }, }, { - name: "GITHUB_TOKEN over blank config", - baseConfig: ``, - GITHUB_TOKEN: "OTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + name: "GITHUB_TOKEN over blank config", + baseConfig: ``, + GITHUB_TOKEN: "OTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "OTOKEN", @@ -58,11 +61,34 @@ func TestInheritEnv(t *testing.T) { }, }, { - name: "GITHUB_TOKEN not applicable to GHE", - baseConfig: ``, - GITHUB_TOKEN: "OTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "example.org", + name: "GH_TOKEN over blank config", + baseConfig: ``, + GH_TOKEN: "OTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "OTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GITHUB_TOKEN not applicable to GHE", + baseConfig: ``, + GITHUB_TOKEN: "OTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"github.com"}, + token: "", + source: "~/.config/gh/config.yml", + writeable: true, + }, + }, + { + name: "GH_TOKEN not applicable to GHE", + baseConfig: ``, + GH_TOKEN: "OTOKEN", + hostname: "example.org", wants: wants{ hosts: []string{"github.com"}, token: "", @@ -73,7 +99,6 @@ func TestInheritEnv(t *testing.T) { { name: "GITHUB_ENTERPRISE_TOKEN over blank config", baseConfig: ``, - GITHUB_TOKEN: "", GITHUB_ENTERPRISE_TOKEN: "ENTOKEN", hostname: "example.org", wants: wants{ @@ -83,6 +108,18 @@ func TestInheritEnv(t *testing.T) { writeable: false, }, }, + { + name: "GH_ENTERPRISE_TOKEN over blank config", + baseConfig: ``, + GH_ENTERPRISE_TOKEN: "ENTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string(nil), + token: "ENTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, { name: "token from file", baseConfig: heredoc.Doc(` @@ -90,9 +127,7 @@ func TestInheritEnv(t *testing.T) { github.com: oauth_token: OTOKEN `), - GITHUB_TOKEN: "", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "OTOKEN", @@ -107,9 +142,8 @@ func TestInheritEnv(t *testing.T) { github.com: oauth_token: OTOKEN `), - GITHUB_TOKEN: "ENVTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + GITHUB_TOKEN: "ENVTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "ENVTOKEN", @@ -117,6 +151,80 @@ func TestInheritEnv(t *testing.T) { writeable: false, }, }, + { + name: "GH_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + github.com: + oauth_token: OTOKEN + `), + GH_TOKEN: "ENVTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "ENVTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GITHUB_ENTERPRISE_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GITHUB_ENTERPRISE_TOKEN: "ENVTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"example.org"}, + token: "ENVTOKEN", + source: "GITHUB_ENTERPRISE_TOKEN", + writeable: false, + }, + }, + { + name: "GH_ENTERPRISE_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GH_ENTERPRISE_TOKEN: "ENVTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"example.org"}, + token: "ENVTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, + { + name: "GH_TOKEN shadows token from GITHUB_TOKEN", + baseConfig: ``, + GH_TOKEN: "GHTOKEN", + GITHUB_TOKEN: "GITHUBTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "GHTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GH_ENTERPRISE_TOKEN shadows token from GITHUB_ENTERPRISE_TOKEN", + baseConfig: ``, + GH_ENTERPRISE_TOKEN: "GHTOKEN", + GITHUB_ENTERPRISE_TOKEN: "GITHUBTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string(nil), + token: "GHTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, { name: "GITHUB_TOKEN adds host entry", baseConfig: heredoc.Doc(` @@ -124,9 +232,8 @@ func TestInheritEnv(t *testing.T) { example.org: oauth_token: OTOKEN `), - GITHUB_TOKEN: "ENVTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + GITHUB_TOKEN: "ENVTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com", "example.org"}, token: "ENVTOKEN", @@ -134,11 +241,48 @@ func TestInheritEnv(t *testing.T) { writeable: false, }, }, + { + name: "GH_TOKEN adds host entry", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GH_TOKEN: "ENVTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com", "example.org"}, + token: "ENVTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN) - os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN) + if tt.GITHUB_TOKEN != "" { + os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN) + } else { + os.Unsetenv("GITHUB_TOKEN") + } + + if tt.GITHUB_ENTERPRISE_TOKEN != "" { + os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN) + } else { + os.Unsetenv("GITHUB_ENTERPRISE_TOKEN") + } + + if tt.GH_TOKEN != "" { + os.Setenv("GH_TOKEN", tt.GH_TOKEN) + } else { + os.Unsetenv("GH_TOKEN") + } + + if tt.GH_ENTERPRISE_TOKEN != "" { + os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN) + } else { + os.Unsetenv("GH_ENTERPRISE_TOKEN") + } baseCfg := NewFromString(tt.baseConfig) cfg := InheritEnv(baseCfg) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 4d64a260b..11bc92766 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -119,9 +119,9 @@ original query accepts an '$endCursor: String' variable and that it fetches the `), Annotations: map[string]string{ "help:environment": heredoc.Doc(` - GITHUB_TOKEN: an authentication token for github.com API requests. + GH_TOKEN, GITHUB_TOKEN (in order of procedence): an authentication token for github.com API requests. - GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. + GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN (in order of procedence): an authentication token for API requests to GitHub Enterprise. GH_HOST: make the request to a GitHub host other than github.com. `), diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index cffd452b9..63bfa7ff7 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -9,10 +9,12 @@ var HelpTopics = map[string]map[string]string{ "environment": { "short": "Environment variables that can be used with gh", "long": heredoc.Doc(` - GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids - being prompted to authenticate and takes precedence over previously stored credentials. + GH_TOKEN, GITHUB_TOKEN (in order of procedence): an authentication token for github.com + API requests. Setting this avoids being prompted to authenticate and takes precedence over + previously stored credentials. - GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. + GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN (in order of precedence): an authentication + token for API requests to GitHub Enterprise. GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands that otherwise operate on a local repository.