Repository: cli/cli
Pull request #9755 (2024-10-14 18:41:13)

Update IsEnterprise and IsTenancy for orthogonality using go-gh
Closes github/cli#577

## Changes

- Add test coverage to places where IsEnterprise incorrectly covers Tenancy
- Upgrade `go-gh` to v2.11.0
- Use `go-gh/auth` package for `IsEnterprise`, `IsTenancy`, and `NormalizeHostname`
- auth: Removed redundant `ghauth.IsTenancy(host)` check

This adds test coverage to ensure orthogonality between `IsEnterprise` and `IsTenancy` functionality, then imports the functions from `go-gh` to fix the tests. There's some additional cleanup beyond this as well. See the commit messages for more details.

## Testing
 
1. Pull down branch `gh pr checkout 9755`
2. Run all tests
3. Login and logout with a tenancy stamp to verify the flow is working: `gh auth login -h <tenancy-instance>`
4. Login and logout of a server stamp to verify the flow is working: `gh auth login -h <server-instance>`


Patch:
diff --git a/api/client.go b/api/client.go
index e32856554..b30f5f164 100644
--- a/api/client.go
+++ b/api/client.go
@@ -10,8 +10,8 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/cli/cli/v2/internal/ghinstance"
 	ghAPI "github.com/cli/go-gh/v2/pkg/api"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 const (
@@ -249,7 +249,7 @@ func generateScopesSuggestion(statusCode int, endpointNeedsScopes, tokenHasScope
 		return fmt.Sprintf(
 			"This API operation needs the %[1]q scope. To request it, run:  gh auth refresh -h %[2]s -s %[1]s",
 			s,
-			ghinstance.NormalizeHostname(hostname),
+			ghauth.NormalizeHostname(hostname),
 		)
 	}
 
diff --git a/api/http_client.go b/api/http_client.go
index f6e133f1f..e9381fa85 100644
--- a/api/http_client.go
+++ b/api/http_client.go
@@ -7,9 +7,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/utils"
 	ghAPI "github.com/cli/go-gh/v2/pkg/api"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 type tokenGetter interface {
@@ -98,7 +98,7 @@ func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper
 			// Only set header if an initial request or redirect request to the same host as the initial request.
 			// If the host has changed during a redirect do not add the authentication token header.
 			if !redirectHostnameChange {
-				hostname := ghinstance.NormalizeHostname(getHost(req))
+				hostname := ghauth.NormalizeHostname(getHost(req))
 				if token, _ := cfg.ActiveToken(hostname); token != "" {
 					req.Header.Set(authorization, fmt.Sprintf("token %s", token))
 				}
diff --git a/go.mod b/go.mod
index 42e3c84e6..874885da6 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/charmbracelet/glamour v0.7.0
 	github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c
-	github.com/cli/go-gh/v2 v2.10.0
+	github.com/cli/go-gh/v2 v2.11.0
 	github.com/cli/oauth v1.1.1
 	github.com/cli/safeexec v1.0.1
 	github.com/cpuguy83/go-md2man/v2 v2.0.5
diff --git a/go.sum b/go.sum
index 9a1fc23ee..28e339f9e 100644
--- a/go.sum
+++ b/go.sum
@@ -95,8 +95,8 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1
 github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
 github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
-github.com/cli/go-gh/v2 v2.10.0 h1:GMflBKoErBXlLvN2euxzL+p7JaM8erlSmw0cT7uZr7M=
-github.com/cli/go-gh/v2 v2.10.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE=
+github.com/cli/go-gh/v2 v2.11.0 h1:TERLYMMWderKBO3lBff/JIu2+eSly2oFRgN2WvO+3eA=
+github.com/cli/go-gh/v2 v2.11.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE=
 github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs=
 github.com/cli/oauth v1.1.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
 github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
diff --git a/internal/authflow/flow.go b/internal/authflow/flow.go
index 52b00284b..1511f6e67 100644
--- a/internal/authflow/flow.go
+++ b/internal/authflow/flow.go
@@ -16,6 +16,8 @@ import (
 	"github.com/cli/cli/v2/utils"
 	"github.com/cli/oauth"
 	"github.com/henvic/httpretty"
+
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 var (
@@ -41,13 +43,6 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
 	minimumScopes := []string{"repo", "read:org", "gist"}
 	scopes := append(minimumScopes, additionalScopes...)
 
-	callbackURI := "http://127.0.0.1/callback"
-	if ghinstance.IsEnterprise(oauthHost) {
-		// the OAuth app on Enterprise hosts is still registered with a legacy callback URL
-		// see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650
-		callbackURI = "http://localhost/"
-	}
-
 	host, err := oauth.NewGitHubHost(ghinstance.HostPrefix(oauthHost))
 	if err != nil {
 		return "", "", err
@@ -57,7 +52,7 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
 		Host:         host,
 		ClientID:     oauthClientID,
 		ClientSecret: oauthClientSecret,
-		CallbackURI:  callbackURI,
+		CallbackURI:  getCallbackURI(oauthHost),
 		Scopes:       scopes,
 		DisplayCode: func(code, verificationURL string) error {
 			fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code))
@@ -110,6 +105,16 @@ func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
 	return token.Token, userLogin, nil
 }
 
+func getCallbackURI(oauthHost string) string {
+	callbackURI := "http://127.0.0.1/callback"
+	if ghauth.IsEnterprise(oauthHost) {
+		// the OAuth app on Enterprise hosts is still registered with a legacy callback URL
+		// see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650
+		callbackURI = "http://localhost/"
+	}
+	return callbackURI
+}
+
 type cfg struct {
 	token string
 }
diff --git a/internal/config/config.go b/internal/config/config.go
index 1b56d30b2..476154d66 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -10,7 +10,7 @@ import (
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/keyring"
 	o "github.com/cli/cli/v2/pkg/option"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	ghConfig "github.com/cli/go-gh/v2/pkg/config"
 )
 
@@ -206,7 +206,7 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
 	if c.tokenOverride != nil {
 		return c.tokenOverride(hostname)
 	}
-	token, source := ghAuth.TokenFromEnvOrConfig(hostname)
+	token, source := ghauth.TokenFromEnvOrConfig(hostname)
 	if token == "" {
 		var err error
 		token, err = c.TokenFromKeyring(hostname)
@@ -240,7 +240,7 @@ func (c *AuthConfig) HasEnvToken() bool {
 	// It has to use a hostname that is not going to be found in the hosts so that it
 	// can guarantee that tokens will only be returned from a set env var.
 	// Discussed here, but maybe worth revisiting: https://github.com/cli/cli/pull/7169#discussion_r1136979033
-	token, _ := ghAuth.TokenFromEnvOrConfig(hostname)
+	token, _ := ghauth.TokenFromEnvOrConfig(hostname)
 	return token != ""
 }
 
@@ -282,7 +282,7 @@ func (c *AuthConfig) Hosts() []string {
 	if c.hostsOverride != nil {
 		return c.hostsOverride()
 	}
-	return ghAuth.KnownHosts()
+	return ghauth.KnownHosts()
 }
 
 // SetHosts will override any hosts resolution and return the given
@@ -297,7 +297,7 @@ func (c *AuthConfig) DefaultHost() (string, string) {
 	if c.defaultHostOverride != nil {
 		return c.defaultHostOverride()
 	}
-	return ghAuth.DefaultHost()
+	return ghauth.DefaultHost()
 }
 
 // SetDefaultHost will override any host resolution and return the given
diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go
index 396d1eb5b..a9bbe25f8 100644
--- a/internal/featuredetection/feature_detection.go
+++ b/internal/featuredetection/feature_detection.go
@@ -4,8 +4,9 @@ import (
 	"net/http"
 
 	"github.com/cli/cli/v2/api"
-	"github.com/cli/cli/v2/internal/ghinstance"
 	"golang.org/x/sync/errgroup"
+
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 type Detector interface {
@@ -62,7 +63,7 @@ func NewDetector(httpClient *http.Client, host string) Detector {
 }
 
 func (d *detector) IssueFeatures() (IssueFeatures, error) {
-	if !ghinstance.IsEnterprise(d.host) {
+	if !ghauth.IsEnterprise(d.host) {
 		return allIssueFeatures, nil
 	}
 
@@ -163,7 +164,7 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
 }
 
 func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
-	if !ghinstance.IsEnterprise(d.host) {
+	if !ghauth.IsEnterprise(d.host) {
 		return allRepositoryFeatures, nil
 	}
 
diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go
index f80bfef57..be839d62e 100644
--- a/internal/ghinstance/host.go
+++ b/internal/ghinstance/host.go
@@ -4,6 +4,8 @@ import (
 	"errors"
 	"fmt"
 	"strings"
+
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 // DefaultHostname is the domain name of the default GitHub instance.
@@ -20,22 +22,10 @@ func Default() string {
 	return defaultHostname
 }
 
-// IsEnterprise reports whether a non-normalized host name looks like a GHE instance.
-func IsEnterprise(h string) bool {
-	normalizedHostName := NormalizeHostname(h)
-	return normalizedHostName != defaultHostname && normalizedHostName != localhost
-}
-
-// IsTenancy reports whether a non-normalized host name looks like a tenancy instance.
-func IsTenancy(h string) bool {
-	normalizedHostName := NormalizeHostname(h)
-	return strings.HasSuffix(normalizedHostName, "."+tenancyHost)
-}
-
 // TenantName extracts the tenant name from tenancy host name and
 // reports whether it found the tenant name.
 func TenantName(h string) (string, bool) {
-	normalizedHostName := NormalizeHostname(h)
+	normalizedHostName := ghauth.NormalizeHostname(h)
 	return cutSuffix(normalizedHostName, "."+tenancyHost)
 }
 
@@ -43,22 +33,6 @@ func isGarage(h string) bool {
 	return strings.EqualFold(h, "garage.github.com")
 }
 
-// NormalizeHostname returns the canonical host name of a GitHub instance.
-func NormalizeHostname(h string) string {
-	hostname := strings.ToLower(h)
-	if strings.HasSuffix(hostname, "."+defaultHostname) {
-		return defaultHostname
-	}
-	if strings.HasSuffix(hostname, "."+localhost) {
-		return localhost
-	}
-	if before, found := cutSuffix(hostname, "."+tenancyHost); found {
-		idx := strings.LastIndex(before, ".")
-		return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
-	}
-	return hostname
-}
-
 func HostnameValidator(hostname string) error {
 	if len(strings.TrimSpace(hostname)) < 1 {
 		return errors.New("a value is required")
@@ -73,14 +47,7 @@ func GraphQLEndpoint(hostname string) string {
 	if isGarage(hostname) {
 		return fmt.Sprintf("https://%s/api/graphql", hostname)
 	}
-	// Once we change Tenancy to no longer be treated as Enterprise, this
-	// conditional can be removed as the flow will fall through to the bottom.
-	// However, we can't do that until we've investigated all places in which
-	// Tenancy is currently treated as Enterprise.
-	if IsTenancy(hostname) {
-		return fmt.Sprintf("https://api.%s/graphql", hostname)
-	}
-	if IsEnterprise(hostname) {
+	if ghauth.IsEnterprise(hostname) {
 		return fmt.Sprintf("https://%s/api/graphql", hostname)
 	}
 	if strings.EqualFold(hostname, localhost) {
@@ -93,14 +60,7 @@ func RESTPrefix(hostname string) string {
 	if isGarage(hostname) {
 		return fmt.Sprintf("https://%s/api/v3/", hostname)
 	}
-	// Once we change Tenancy to no longer be treated as Enterprise, this
-	// conditional can be removed as the flow will fall through to the bottom.
-	// However, we can't do that until we've investigated all places in which
-	// Tenancy is currently treated as Enterprise.
-	if IsTenancy(hostname) {
-		return fmt.Sprintf("https://api.%s/", hostname)
-	}
-	if IsEnterprise(hostname) {
+	if ghauth.IsEnterprise(hostname) {
 		return fmt.Sprintf("https://%s/api/v3/", hostname)
 	}
 	if strings.EqualFold(hostname, localhost) {
@@ -121,7 +81,7 @@ func GistHost(hostname string) string {
 	if isGarage(hostname) {
 		return fmt.Sprintf("%s/gist/", hostname)
 	}
-	if IsEnterprise(hostname) {
+	if ghauth.IsEnterprise(hostname) {
 		return fmt.Sprintf("%s/gist/", hostname)
 	}
 	if strings.EqualFold(hostname, localhost) {
diff --git a/internal/ghrepo/repo.go b/internal/ghrepo/repo.go
index 4f0328e99..a31d354b5 100644
--- a/internal/ghrepo/repo.go
+++ b/internal/ghrepo/repo.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/cli/cli/v2/internal/ghinstance"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	"github.com/cli/go-gh/v2/pkg/repository"
 )
 
@@ -37,7 +37,7 @@ func FullName(r Interface) string {
 }
 
 func defaultHost() string {
-	host, _ := ghAuth.DefaultHost()
+	host, _ := ghauth.DefaultHost()
 	return host
 }
 
diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go
index 1480a29a0..653c9e6c0 100644
--- a/pkg/cmd/auth/login/login.go
+++ b/pkg/cmd/auth/login/login.go
@@ -15,7 +15,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	"github.com/spf13/cobra"
 )
 
@@ -123,7 +123,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
 			}
 
 			if opts.Hostname == "" && (!opts.Interactive || opts.Web) {
-				opts.Hostname, _ = ghAuth.DefaultHost()
+				opts.Hostname, _ = ghauth.DefaultHost()
 			}
 
 			opts.MainExecutable = f.Executable()
diff --git a/pkg/cmd/gist/clone/clone.go b/pkg/cmd/gist/clone/clone.go
index 6fa6f226a..c7db74521 100644
--- a/pkg/cmd/gist/clone/clone.go
+++ b/pkg/cmd/gist/clone/clone.go
@@ -8,11 +8,12 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/gh"
-	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
+
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 type CloneOptions struct {
@@ -93,7 +94,7 @@ func cloneRun(opts *CloneOptions) error {
 }
 
 func formatRemoteURL(hostname string, gistID string, protocol string) string {
-	if ghinstance.IsEnterprise(hostname) {
+	if ghauth.IsEnterprise(hostname) {
 		if protocol == "ssh" {
 			return fmt.Sprintf("git@%s:gist/%s.git", hostname, gistID)
 		}
diff --git a/pkg/cmd/pr/status/http.go b/pkg/cmd/pr/status/http.go
index 1eb12ed74..b3fbbac26 100644
--- a/pkg/cmd/pr/status/http.go
+++ b/pkg/cmd/pr/status/http.go
@@ -6,9 +6,10 @@ import (
 	"strings"
 
 	"github.com/cli/cli/v2/api"
-	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/set"
+
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 type requestOptions struct {
@@ -120,13 +121,10 @@ func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options r
 		}
 	`
 
-	currentUsername := options.Username
-	if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) {
-		var err error
-		currentUsername, err = api.CurrentLoginName(apiClient, repo.RepoHost())
-		if err != nil {
-			return nil, err
-		}
+	currentUsername, err := getCurrentUsername(options.Username, repo.RepoHost(), apiClient)
+
+	if err != nil {
+		return nil, err
 	}
 
 	viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername)
@@ -188,6 +186,17 @@ func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options r
 	return &payload, nil
 }
 
+func getCurrentUsername(username string, hostname string, apiClient *api.Client) (string, error) {
+	if username == "@me" && ghauth.IsEnterprise(hostname) {
+		var err error
+		username, err = api.CurrentLoginName(apiClient, hostname)
+		if err != nil {
+			return "", err
+		}
+	}
+	return username, nil
+}
+
 func pullRequestFragment(conflictStatus bool, statusCheckRollupWithCountByState bool) (string, error) {
 	fields := []string{
 		"number", "title", "state", "url", "isDraft", "isCrossRepository",
diff --git a/pkg/cmd/repo/delete/delete.go b/pkg/cmd/repo/delete/delete.go
index 1e96e404d..722d748b9 100644
--- a/pkg/cmd/repo/delete/delete.go
+++ b/pkg/cmd/repo/delete/delete.go
@@ -10,7 +10,7 @@ import (
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	"github.com/spf13/cobra"
 )
 
@@ -85,7 +85,7 @@ func deleteRun(opts *DeleteOptions) error {
 	} else {
 		repoSelector := opts.RepoArg
 		if !strings.Contains(repoSelector, "/") {
-			defaultHost, _ := ghAuth.DefaultHost()
+			defaultHost, _ := ghauth.DefaultHost()
 			currentUser, err := api.CurrentLoginName(apiClient, defaultHost)
 			if err != nil {
 				return err
diff --git a/pkg/cmd/ruleset/list/list.go b/pkg/cmd/ruleset/list/list.go
index 7cc533e13..2b945c906 100644
--- a/pkg/cmd/ruleset/list/list.go
+++ b/pkg/cmd/ruleset/list/list.go
@@ -15,7 +15,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/ruleset/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	"github.com/spf13/cobra"
 )
 
@@ -109,7 +109,7 @@ func listRun(opts *ListOptions) error {
 		}
 	}
 
-	hostname, _ := ghAuth.DefaultHost()
+	hostname, _ := ghauth.DefaultHost()
 
 	if opts.WebMode {
 		var rulesetURL string
diff --git a/pkg/cmd/ruleset/view/view.go b/pkg/cmd/ruleset/view/view.go
index d272d273e..cefd926ab 100644
--- a/pkg/cmd/ruleset/view/view.go
+++ b/pkg/cmd/ruleset/view/view.go
@@ -15,7 +15,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/ruleset/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	ghAuth "github.com/cli/go-gh/v2/pkg/auth"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 	"github.com/spf13/cobra"
 )
 
@@ -124,7 +124,7 @@ func viewRun(opts *ViewOptions) error {
 		}
 	}
 
-	hostname, _ := ghAuth.DefaultHost()
+	hostname, _ := ghauth.DefaultHost()
 	cs := opts.IO.ColorScheme()
 
 	if opts.InteractiveMode {


Test patch:
diff --git a/internal/authflow/flow_test.go b/internal/authflow/flow_test.go
new file mode 100644
index 000000000..b7ba1f64a
--- /dev/null
+++ b/internal/authflow/flow_test.go
@@ -0,0 +1,36 @@
+package authflow
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_getCallbackURI(t *testing.T) {
+	tests := []struct {
+		name      string
+		oauthHost string
+		want      string
+	}{
+		{
+			name:      "dotcom",
+			oauthHost: "github.com",
+			want:      "http://127.0.0.1/callback",
+		},
+		{
+			name:      "ghes",
+			oauthHost: "my.server.com",
+			want:      "http://localhost/",
+		},
+		{
+			name:      "ghec data residency (ghe.com)",
+			oauthHost: "stampname.ghe.com",
+			want:      "http://127.0.0.1/callback",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, getCallbackURI(tt.oauthHost))
+		})
+	}
+}
diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go
index 2a32e9a2d..8af091c3f 100644
--- a/internal/featuredetection/feature_detection_test.go
+++ b/internal/featuredetection/feature_detection_test.go
@@ -25,6 +25,14 @@ func TestIssueFeatures(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name:     "ghec data residency (ghe.com)",
+			hostname: "stampname.ghe.com",
+			wantFeatures: IssueFeatures{
+				StateReason: true,
+			},
+			wantErr: false,
+		},
 		{
 			name:     "GHE empty response",
 			hostname: "git.my.org",
@@ -271,6 +279,16 @@ func TestRepositoryFeatures(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name:     "ghec data residency (ghe.com)",
+			hostname: "stampname.ghe.com",
+			wantFeatures: RepositoryFeatures{
+				PullRequestTemplateQuery: true,
+				VisibilityField:          true,
+				AutoMerge:                true,
+			},
+			wantErr: false,
+		},
 		{
 			name:     "GHE empty response",
 			hostname: "git.my.org",
diff --git a/internal/ghinstance/host_test.go b/internal/ghinstance/host_test.go
index e9fb97008..1b7e0146d 100644
--- a/internal/ghinstance/host_test.go
+++ b/internal/ghinstance/host_test.go
@@ -6,88 +6,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestIsEnterprise(t *testing.T) {
-	tests := []struct {
-		host string
-		want bool
-	}{
-		{
-			host: "github.com",
-			want: false,
-		},
-		{
-			host: "api.github.com",
-			want: false,
-		},
-		{
-			host: "github.localhost",
-			want: false,
-		},
-		{
-			host: "api.github.localhost",
-			want: false,
-		},
-		{
-			host: "garage.github.com",
-			want: false,
-		},
-		{
-			host: "ghe.io",
-			want: true,
-		},
-		{
-			host: "example.com",
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.host, func(t *testing.T) {
-			if got := IsEnterprise(tt.host); got != tt.want {
-				t.Errorf("IsEnterprise() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func TestIsTenancy(t *testing.T) {
-	tests := []struct {
-		host string
-		want bool
-	}{
-		{
-			host: "github.com",
-			want: false,
-		},
-		{
-			host: "github.localhost",
-			want: false,
-		},
-		{
-			host: "garage.github.com",
-			want: false,
-		},
-		{
-			host: "ghe.com",
-			want: false,
-		},
-		{
-			host: "tenant.ghe.com",
-			want: true,
-		},
-		{
-			host: "api.tenant.ghe.com",
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.host, func(t *testing.T) {
-			if got := IsTenancy(tt.host); got != tt.want {
-				t.Errorf("IsTenancy() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestTenantName(t *testing.T) {
 	tests := []struct {
 		host       string
@@ -130,69 +48,6 @@ func TestTenantName(t *testing.T) {
 	}
 }
 
-func TestNormalizeHostname(t *testing.T) {
-	tests := []struct {
-		host string
-		want string
-	}{
-		{
-			host: "GitHub.com",
-			want: "github.com",
-		},
-		{
-			host: "api.github.com",
-			want: "github.com",
-		},
-		{
-			host: "ssh.github.com",
-			want: "github.com",
-		},
-		{
-			host: "upload.github.com",
-			want: "github.com",
-		},
-		{
-			host: "GitHub.localhost",
-			want: "github.localhost",
-		},
-		{
-			host: "api.github.localhost",
-			want: "github.localhost",
-		},
-		{
-			host: "garage.github.com",
-			want: "github.com",
-		},
-		{
-			host: "GHE.IO",
-			want: "ghe.io",
-		},
-		{
-			host: "git.my.org",
-			want: "git.my.org",
-		},
-		{
-			host: "ghe.com",
-			want: "ghe.com",
-		},
-		{
-			host: "tenant.ghe.com",
-			want: "tenant.ghe.com",
-		},
-		{
-			host: "api.tenant.ghe.com",
-			want: "tenant.ghe.com",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.host, func(t *testing.T) {
-			if got := NormalizeHostname(tt.host); got != tt.want {
-				t.Errorf("NormalizeHostname() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestHostnameValidator(t *testing.T) {
 	tests := []struct {
 		name     string
diff --git a/pkg/cmd/attestation/auth/host.go b/pkg/cmd/attestation/auth/host.go
index a40df8335..d72f82fa5 100644
--- a/pkg/cmd/attestation/auth/host.go
+++ b/pkg/cmd/attestation/auth/host.go
@@ -3,15 +3,13 @@ package auth
 import (
 	"errors"
 
-	"github.com/cli/cli/v2/internal/ghinstance"
+	ghauth "github.com/cli/go-gh/v2/pkg/auth"
 )
 
 var ErrUnsupportedHost = errors.New("An unsupported host was detected. Note that gh attestation does not currently support GHES")
 
 func IsHostSupported(host string) error {
-	// Note that this check is slightly redundant as Tenancy should not be considered Enterprise
-	// but the ghinstance package has not been updated to reflect this yet.
-	if ghinstance.IsEnterprise(host) && !ghinstance.IsTenancy(host) {
+	if ghauth.IsEnterprise(host) {
 		return ErrUnsupportedHost
 	}
 	return nil
diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go
index 867ff8c1b..c548b2ec7 100644
--- a/pkg/cmd/attestation/inspect/inspect.go
+++ b/pkg/cmd/attestation/inspect/inspect.go
@@ -85,7 +85,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
 				Logger: opts.Logger,
 			}
 			// Prepare for tenancy if detected
-			if ghinstance.IsTenancy(opts.Hostname) {
+			if ghauth.IsTenancy(opts.Hostname) {
 				hc, err := f.HttpClient()
 				if err != nil {
 					return err
diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go
index 5473adc0c..c092b81ec 100644
--- a/pkg/cmd/attestation/trustedroot/trustedroot.go
+++ b/pkg/cmd/attestation/trustedroot/trustedroot.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/pkg/cmd/attestation/api"
 	"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
 	"github.com/cli/cli/v2/pkg/cmd/attestation/io"
@@ -68,7 +67,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
 				return err
 			}
 
-			if ghinstance.IsTenancy(opts.Hostname) {
+			if ghauth.IsTenancy(opts.Hostname) {
 				c, err := f.Config()
 				if err != nil {
 					return err
diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go
index 17b8a84d1..d14081dd8 100644
--- a/pkg/cmd/attestation/verify/verify.go
+++ b/pkg/cmd/attestation/verify/verify.go
@@ -138,7 +138,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
 			}
 
 			// Prepare for tenancy if detected
-			if ghinstance.IsTenancy(opts.Hostname) {
+			if ghauth.IsTenancy(opts.Hostname) {
 				td, err := opts.APIClient.GetTrustDomain()
 				if err != nil {
 					return fmt.Errorf("error getting trust domain, make sure you are authenticated against the host: %w", err)
diff --git a/pkg/cmd/pr/status/http_test.go b/pkg/cmd/pr/status/http_test.go
new file mode 100644
index 000000000..eeb91e909
--- /dev/null
+++ b/pkg/cmd/pr/status/http_test.go
@@ -0,0 +1,62 @@
+package status
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_getCurrentUsername(t *testing.T) {
+	tests := []struct {
+		name            string
+		username        string
+		hostname        string
+		serverUsername  string
+		currentUsername string
+	}{
+		{
+			name:            "dotcom",
+			username:        "@me",
+			hostname:        "github.com",
+			currentUsername: "@me",
+		},
+		{
+			name:            "ghec data residency (ghe.com)",
+			username:        "@me",
+			hostname:        "stampname.ghe.com",
+			currentUsername: "@me",
+		},
+		{
+			name:            "ghes",
+			username:        "@me",
+			hostname:        "my.server.com",
+			serverUsername:  "@serverUserName",
+			currentUsername: "@serverUserName",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			clientStub := &http.Client{}
+			if tt.serverUsername != "" {
+				reg := &httpmock.Registry{}
+				defer reg.Verify(t)
+
+				reg.Register(
+					httpmock.GraphQL(`query UserCurrent\b`),
+					httpmock.StringResponse(`{"data":{"viewer":{"login":"`+tt.serverUsername+`"}}}`),
+				)
+
+				clientStub.Transport = reg
+			}
+
+			apiClientStub := api.NewClientFromHTTP(clientStub)
+
+			currentUsername, err := getCurrentUsername(tt.username, tt.hostname, apiClientStub)
+			assert.NoError(t, err)
+			assert.Equal(t, tt.currentUsername, currentUsername)
+		})
+	}
+}